Skip to content

Commit

Permalink
Merge pull request #818 from Automattic/task/add-auto-scroll-discover…
Browse files Browse the repository at this point in the history
…-carousal

Add auto scroll to discover featured carousal
  • Loading branch information
ashiagr authored Mar 10, 2023
2 parents cbc7784 + 0fd13a5 commit f6802e1
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 6 deletions.
1 change: 1 addition & 0 deletions base.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ android {
// Feature Flags
buildConfigField "boolean", "END_OF_YEAR_ENABLED", "false"
buildConfigField "boolean", "SINGLE_SIGN_ON_ENABLED", "false"
buildConfigField "boolean", "DISCOVER_FEATURED_AUTO_SCROLL", "false"

testInstrumentationRunner project.testInstrumentationRunner
testApplicationId "au.com.shiftyjelly.pocketcasts.test" + project.name.replace("-", "_")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package au.com.shiftyjelly.pocketcasts.discover.util

import java.util.Timer
import java.util.TimerTask

private const val AUTO_SCROLL_INTERVAL = 5000L

class AutoScrollHelper(private val onAutoScrollCompleted: () -> Unit) {
private var autoScrollTimer: Timer? = null
private var autoScrollTimerTask: TimerTask? = null
private var skipAutoScroll = false

fun startAutoScrollTimer(delay: Long = 0L) {
if (autoScrollTimerTask != null) return
autoScrollTimerTask = object : TimerTask() {
override fun run() {
if (!skipAutoScroll) onAutoScrollCompleted()
skipAutoScroll = false
}
}
autoScrollTimer = Timer().apply {
schedule(autoScrollTimerTask, delay, AUTO_SCROLL_INTERVAL)
}
}

fun stopAutoScrollTimer() {
autoScrollTimer?.cancel()
autoScrollTimerTask?.cancel()
autoScrollTimer = null
autoScrollTimerTask = null
}

fun skipAutoScroll() {
skipAutoScroll = true
}

companion object {
const val AUTO_SCROLL_DELAY = 5000L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package au.com.shiftyjelly.pocketcasts.discover.util

import android.content.Context
import android.util.DisplayMetrics
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView

/*
1. Increases scrolling speed of recyclerView.smoothScrollToPosition(position)
2. Sets custom extra layout space for pre caching an extra page
*/
class ScrollingLinearLayoutManager(
context: Context?,
orientation: Int,
reverseLayout: Boolean,
) : LinearLayoutManager(context, orientation, reverseLayout) {
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State,
position: Int,
) {
val linearSmoothScroller: LinearSmoothScroller =
object : LinearSmoothScroller(recyclerView.context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
linearSmoothScroller.targetPosition = position
startSmoothScroll(linearSmoothScroller)
}

/* By default, LinearLayoutManager lays out 1 extra page of items while smooth scrolling, in the direction of the scroll.
This behavior is overridden to lay out an extra page even on a manual swipe. */
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
extraLayoutSpace[0] = 0
extraLayoutSpace[1] = this.width
}

companion object {
private const val MILLISECONDS_PER_INCH = 100f // default is 25f (bigger = slower)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsEvent
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsTrackerWrapper
import au.com.shiftyjelly.pocketcasts.analytics.FirebaseAnalyticsTracker
Expand All @@ -27,13 +28,16 @@ import au.com.shiftyjelly.pocketcasts.discover.databinding.RowPodcastSmallListBi
import au.com.shiftyjelly.pocketcasts.discover.databinding.RowSingleEpisodeBinding
import au.com.shiftyjelly.pocketcasts.discover.databinding.RowSinglePodcastBinding
import au.com.shiftyjelly.pocketcasts.discover.extensions.updateSubscribeButtonIcon
import au.com.shiftyjelly.pocketcasts.discover.util.AutoScrollHelper
import au.com.shiftyjelly.pocketcasts.discover.util.ScrollingLinearLayoutManager
import au.com.shiftyjelly.pocketcasts.discover.view.DiscoverFragment.Companion.EPISODE_UUID_KEY
import au.com.shiftyjelly.pocketcasts.discover.view.DiscoverFragment.Companion.LIST_ID_KEY
import au.com.shiftyjelly.pocketcasts.discover.view.DiscoverFragment.Companion.PODCAST_UUID_KEY
import au.com.shiftyjelly.pocketcasts.discover.viewmodel.PodcastList
import au.com.shiftyjelly.pocketcasts.localization.helper.TimeHelper
import au.com.shiftyjelly.pocketcasts.localization.helper.tryToLocalise
import au.com.shiftyjelly.pocketcasts.models.entity.Episode
import au.com.shiftyjelly.pocketcasts.preferences.BuildConfig
import au.com.shiftyjelly.pocketcasts.repositories.images.into
import au.com.shiftyjelly.pocketcasts.servers.cdn.ArtworkColors
import au.com.shiftyjelly.pocketcasts.servers.cdn.StaticServerManagerImpl
Expand Down Expand Up @@ -65,6 +69,8 @@ import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import au.com.shiftyjelly.pocketcasts.localization.R as LR
Expand All @@ -73,6 +79,7 @@ import au.com.shiftyjelly.pocketcasts.ui.R as UR
private const val MAX_ROWS_SMALL_LIST = 20
private const val CURRENT_PAGE = "current_page"
private const val TOTAL_PAGES = "total_pages"
private const val INITIAL_PREFETCH_COUNT = 1

internal data class ChangeRegionRow(val region: DiscoverRegion)

Expand Down Expand Up @@ -163,33 +170,106 @@ internal class DiscoverAdapter(
}

inner class CarouselListViewHolder(var binding: RowCarouselListBinding) : NetworkLoadableViewHolder(binding.root) {
private var autoScrollHelper: AutoScrollHelper? = null
private val scrollListener = object : OnScrollListener() {
private var draggingStarted: Boolean = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_SETTLING -> Unit // Do nothing
RecyclerView.SCROLL_STATE_DRAGGING -> {
draggingStarted = true
autoScrollHelper?.stopAutoScrollTimer()
}
RecyclerView.SCROLL_STATE_IDLE -> {
if (draggingStarted) {
/* Start auto scroll with a delay after a manual swipe */
autoScrollHelper?.startAutoScrollTimer(delay = AutoScrollHelper.AUTO_SCROLL_DELAY)
draggingStarted = false
}
}
}
}
}

val adapter = CarouselListRowAdapter(null, theme, listener::onPodcastClicked, listener::onPodcastSubscribe, analyticsTracker)

private val linearLayoutManager =
LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false).apply {
initialPrefetchItemCount = 1
private val scrollingLayoutManager =
ScrollingLinearLayoutManager(
itemView.context,
RecyclerView.HORIZONTAL,
false
).apply {
initialPrefetchItemCount = INITIAL_PREFETCH_COUNT
}

init {
recyclerView?.layoutManager = linearLayoutManager
recyclerView?.layoutManager = scrollingLayoutManager
recyclerView?.itemAnimator = null
recyclerView?.addOnScrollListener(scrollListener)

if (BuildConfig.DISCOVER_FEATURED_AUTO_SCROLL) {
autoScrollHelper = AutoScrollHelper {
if (adapter.itemCount == 0) return@AutoScrollHelper
val currentPosition = binding.pageIndicatorView.position
val nextPosition = (currentPosition + 1)
.takeIf { it < adapter.itemCount } ?: 0
MainScope().launch {
if (nextPosition > currentPosition) {
recyclerView?.smoothScrollToPosition(nextPosition)
} else {
/* Jump to the beginning to avoid a backward scroll animation */
recyclerView?.scrollToPosition(nextPosition)
}
binding.pageIndicatorView.position = nextPosition
trackPageChanged(nextPosition)
}
}
}

val snapHelper = HorizontalPeekSnapHelper(0)
snapHelper.attachToRecyclerView(recyclerView)
snapHelper.onSnapPositionChanged = { position ->
/* Page just snapped, skip auto scroll */
autoScrollHelper?.skipAutoScroll()
binding.pageIndicatorView.position = position
analyticsTracker.track(AnalyticsEvent.DISCOVER_FEATURED_PAGE_CHANGED, mapOf(CURRENT_PAGE to position, TOTAL_PAGES to adapter.itemCount))
trackPageChanged(position)
}

recyclerView?.adapter = adapter
adapter.submitList(listOf(LoadingItem()))

itemView.viewTreeObserver?.apply {
/* Stop auto scroll when app is backgrounded */
addOnWindowFocusChangeListener { hasFocus ->
if (!hasFocus) autoScrollHelper?.stopAutoScrollTimer()
}
/* Manage auto scroll when itemView's visibility changes on going to next screen */
addOnGlobalLayoutListener {
if (itemView.isShown) {
autoScrollHelper?.startAutoScrollTimer()
} else {
autoScrollHelper?.stopAutoScrollTimer()
}
}
}
}

override fun onRestoreInstanceState(state: Parcelable?) {
super.onRestoreInstanceState(state)
recyclerView?.post {
binding.pageIndicatorView.position = linearLayoutManager.findFirstVisibleItemPosition()
binding.pageIndicatorView.position = scrollingLayoutManager.findFirstVisibleItemPosition()
recyclerView.scrollToPosition(binding.pageIndicatorView.position)
}
}

private fun trackPageChanged(position: Int) {
analyticsTracker.track(
AnalyticsEvent.DISCOVER_FEATURED_PAGE_CHANGED,
mapOf(CURRENT_PAGE to position, TOTAL_PAGES to adapter.itemCount)
)
}
}

inner class SmallListViewHolder(val binding: RowPodcastSmallListBinding) : NetworkLoadableViewHolder(binding.root), ShowAllRow {
Expand Down

0 comments on commit f6802e1

Please sign in to comment.