Android RecyclerView: Fake smoothScroll to top if many, many items
Asked Answered
A

2

4

We have your standard RecyclerView-based Feed screen in our app.

If I have like 100+ items in my RecyclerView and press the 'go to top' shortcut, the default behaviour for smoothScrollToPosition(0) is to take a very long time to scroll to the top.

It's almost comical how long it can take - 10 seconds of intensely fast scrolling to the top if you've gone down far enough (which is a common use case)!

We're looking for a way to "fake" the scroll to the top, if the number of items in the RecyclerView > SOME_THRESHOLD.

I'm not an iOS guy, but our iOS version (as the devs tell me) seems to have such behaviour baked into the control. If there are too many items, it'll just do a super quick blurry scroll-up which clearly fakes/omits many of the items in the middle.

Does the RecyclerView have any such capabilities?

We've thought of doing a multi-part thing where we quickly jump to the item at index SOME_THRESHOLD and then call smoothScrollToPosition(0) - you get the idea - but there are drawbacks to most of the things that we've thought of.

Help is appreciated, thank you.

Anse answered 16/12, 2016 at 1:54 Comment(0)
S
5

Solution for controlling the speed of the scroll, to scroll faster to more distant positions

A bit late to the party, but here's a Kotlin solution for anyone else looking.

This proves tricky to solve by overriding calculateSpeedPerPixel of the LinearSmoothScroller. With that attempt for a solution I was getting a lot of "RecyclerView passed over target position" errors. If anyone has an idea how to solve those please share.

I took a different approach with this solution: first jump to a position closer to the target position and then smooth scroll:

/**
 * Enables still giving an impression of difference in scroll depending on how far the item scrolled to is,
 * while not having that tedious huge linear scroll time for distant items.
 * 
 * If scrolling to a position more than minJumpPosition diff away from current position, then jump closer first and then smooth scroll.
 * The distance how far from the target position to jump to is determined by a logarithmic function,
 * which in our case is y=20 at x=20 and for all practical purposes never goes over a y=100 (@x~1000) (so max distance is 100).
 * 
 * If the diff is under 20 there is no jump - for diff 15 the scroll distance is 15 items.
 * If the diff (x) is 30, jumpDiff (y) is around 28, so jump 2 and scroll 28 items.
 * If the diff (x) is 65, jumpDiff (y) is around 44, so jump 21 and scroll 44 items.
 * If the diff (x) is 100, jumpDiff (y) is around 53, so jump 47 and scroll 53 items.
 * If the diff (x) is 380, jumpDiff (y) is around 80, so jump 300 and scroll 80 items.
 * If the diff (x) is 1000, jumpDiff (y) is around 100 items scroll distance.
 * If the diff (x) is 5000, jumpDiff (y) is around 133 items scroll distance.
 * If the diff (x) is 8000, jumpDiff (y) is around 143 items scroll distance.
 * If the diff (x) is 10000, jumpDiff (y) is around 147 items scroll distance.
 *
 * You can play with the parameters to change the:
 *  - minJumpPosition: change when to start applying the jump
 *  - maxScrollAllowed: change speed change rate (steepness of the curve)
 *  - maxPracticalPosition: change what is the highest expected number of items
 * You might find it easier with a visual tool:
 * https://www.desmos.com/calculator/auubsajefh
 */
fun RecyclerView.jumpThenSmoothScroll(smoothScroller: SmoothScroller, position: Int,
                                      delay: Long = 0L,
                                      doAfterScrolled: (() -> Unit)? = null) {
    smoothScroller.targetPosition = position

    val layoutManager = layoutManager as LinearLayoutManager

    fun smoothScrollAndDoAfter() {
        layoutManager.startSmoothScroll(smoothScroller)
        doAfterScrolled?.let { post { postDelayed({ doAfterScrolled() }, max(0L, delay)) } }
    }

    val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()

    val diff = abs(position - firstVisiblePosition).toFloat()

    // Position from which to start applying "jump then scroll".
    val minJumpPosition = 20f

    if (diff > minJumpPosition) {
        // On the logarithmic function graph, 
        // y=minJumpPosition when x=minJumpPosition, and y=maxScrollAllowed when x=maxPracticalPosition.
        // So we are using two points to determine the function: 
        // (minJumpPosition, minJumpPosition) and (maxPracticalPosition, maxScrollAllowed)
        // In our case (20, 20) and (1000, 100)

        // Max practical possible items (max diff between current and target position).
        // It is OK for this to be high as logarithmic function is long approaching this value.
        val maxPracticalPosition = 1000
        // Never scroll more than this number of items. 
        // Scroll will be from 0 to maxScrollAllowed for all practical purposes 
        // ("practical" as determined by maxPracticalPosition).
        val maxScrollAllowed = 100

        // b = (x2/x1)^(1f/(y2-y1))
        val logBase = (maxPracticalPosition / minJumpPosition).pow (1f / (maxScrollAllowed - minJumpPosition))
        // s = (log(b)x1) - y1
        val logShift = log(minJumpPosition, logBase) - minJumpPosition

        val jumpDiff = (log(diff, logBase) - logShift).toInt() // y: 20 to 100 (for diff x: 20 to 1000)
        val jumpDiffDirection = if (position < firstVisiblePosition) 1 else -1
        val jumpPosition = position + (jumpDiff * jumpDiffDirection)

        // First jump closer
        layoutManager.scrollToPositionWithOffset(jumpPosition, 0)
        // Then smooth scroll
        smoothScrollAndDoAfter()
    } else {
        smoothScrollAndDoAfter()
    }
}
Sake answered 29/8, 2020 at 3:11 Comment(3)
Awesome solution!Scantling
Thanks @Mars; If you find it useful, please vote the solution up so others can find it as well.Sake
Works amazing!!Figueroa
O
1

This is a minimal version of @Vlad's answer. If the scroll-distance is smaller than the threshold, it scrolls there, else it jumps there.

Usage:

myRecycler.smoothScrollOrJumpTo(
    goalPosition = position,
    firstVisiblePosition = myLayoutManager.findFirstVisibleItemPosition()
)

Code:

private fun RecyclerView.smoothScrollOrJumpTo(
    goalPosition: Int,
    firstVisiblePosition: Int,
    threshold: Int,
){
    val diff = abs(goalPosition - firstVisiblePosition)

    if(diff < threshold){
        smoothScrollToPosition(goalPosition)
    } else {
        scrollToPosition(goalPosition)
    }
}

Ofc you can go further; e.g. fading out the recyclerview before jumping

Orson answered 24/11, 2020 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.