174

On a RecyclerView, I am able to suddenly scroll to the top of a selected item by using:

((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(position, 0);

However, this abruptly moves the item to the top position. I want to move to the top of an item smoothly.

I've also tried:

recyclerView.smoothScrollToPosition(position);

but it does not work well as it does not move the item to the position selected to the top. It merely scrolls the list until the item on the position is visible.

14 Answers14

325

RecyclerView is designed to be extensible, so there is no need to subclass the LayoutManager (as droidev suggested) just to perform the scrolling.

Instead, just create a SmoothScroller with the preference SNAP_TO_START:

RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(context) {
  @Override protected int getVerticalSnapPreference() {
    return LinearSmoothScroller.SNAP_TO_START;
  }
};

Now you set the position where you want to scroll to:

smoothScroller.setTargetPosition(position);

and pass that SmoothScroller to the LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Shishram
  • 1,514
  • 11
  • 20
Paul Woitaschek
  • 6,717
  • 5
  • 33
  • 52
  • 10
    Thanks for this shorter solution. Just two more things I had to consider in my implementation: As I have a horizontal scrolling view I had to set `protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }`. Furthermore I had to implement the abstract method `public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }`. – AustrianDude Jul 11 '17 at 09:12
  • 1
    @AustrianDude `computeScrollVectorForPosition` is not abstract. Also it returns `layoutManager.computeScrollVectorForPosition` by default. – Paul Woitaschek Jul 11 '17 at 12:24
  • Okay. I am unfortunately using the v7 support library 23.4.0. This must have been changed in a later version. – AustrianDude Jul 11 '17 at 13:28
  • 7
    RecyclerView may be designed to be extensible, but a simple thing like this just feels lazy to be missing. Good answer tho! Thank you. – Michael Jul 13 '17 at 02:45
  • 14
    Is there any way to let it snap to start, but with an offset of X dp? – Mark Buikema May 01 '18 at 12:03
  • 1
    @PaulWoitaschek couldn't this approach create problems? I mean, your code I'm sure will work, but basically you're suggesting to bypass the RecyclerView's _smoothScrollToPosition()_ and instead call the scroll directly via the layout manager, so any internal check potentially done by _smoothScrollToPosition()_ is lost. Android docs state instead: "RecyclerView.LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override smoothScrollToPosition(RecyclerView, State, int) in your LayoutManager." – Alessio Jul 12 '18 at 06:02
  • 2
    @Alessio exactly, this will break RecyclerView's default smoothScrollToPosition functionality – droidev Jul 12 '18 at 13:05
  • @PaulWoitaschek How can I know when to show the button to Scroll to the top using `recyclerView.addOnScrollListener` ? –  Aug 31 '18 at 20:23
  • 9
    is it possible to slow down its speed? – Alireza Noorali Dec 24 '18 at 12:38
  • @PaulWoitaschek it' working but i want to make item in centre like i have horizontal recyclerview and maximum 3 items are visible on the screen.so when i clicked on any other item i want that item in middle/centre how to do that? – Rucha Bhatt Joshi Mar 18 '19 at 13:53
  • 3
    Also wondering if the speed at which the scroll occurs can be modified (slower) @AlirezaNoorali – vikzilla Nov 09 '19 at 00:41
  • @vikzilla To control speed, see my answer below. – Bad Loser Nov 26 '19 at 07:28
  • its ok but if you using NestedScrollView then it dont work – Marjan Davodinejad Dec 17 '19 at 10:09
  • @MarkBuikema we can override `LinearSmoothScroller.calculateDtToFit` to let it snap to start and with offset x dp. – aolphn Jun 22 '21 at 12:44
  • 1
    I tried to use this approach inside onBindViewHolder() during .notifyItemChanged() and it always scrolled list to the very end but not to the target position. So be careful. – PavelGP Nov 06 '21 at 12:30
  • Finally! A succinct solution to a long-standing problem. Thank you for this. – Victor Ude Jul 14 '23 at 15:46
122

for this you have to create a custom LayoutManager

public class LinearLayoutManagerWithSmoothScroller extends LinearLayoutManager {

    public LinearLayoutManagerWithSmoothScroller(Context context) {
        super(context, VERTICAL, false);
    }

    public LinearLayoutManagerWithSmoothScroller(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                                       int position) {
        RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }

    private class TopSnappedSmoothScroller extends LinearSmoothScroller {
        public TopSnappedSmoothScroller(Context context) {
            super(context);

        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return LinearLayoutManagerWithSmoothScroller.this
                    .computeScrollVectorForPosition(targetPosition);
        }

        @Override
        protected int getVerticalSnapPreference() {
            return SNAP_TO_START;
        }
    }
}

use this for your RecyclerView and call smoothScrollToPosition.

example :

 recyclerView.setLayoutManager(new LinearLayoutManagerWithSmoothScroller(context));
 recyclerView.smoothScrollToPosition(position);

this will scroll to top of the RecyclerView item of specified position.

starball
  • 20,030
  • 7
  • 43
  • 238
droidev
  • 7,352
  • 11
  • 62
  • 94
  • I had trouble implementing the suggested LayoutManager. This answer here worked much easier for me: http://stackoverflow.com/questions/28025425/android-recyclerview-smooth-scroll-to-view-thats-animating-their-height – arberg Dec 21 '15 at 07:24
  • 3
    Only useful answer after hours of pain, this works as designed! – wblaschko Jun 18 '16 at 00:06
  • 1
    @droidev I've used your example with smooth scrolling and generally SNAP_TO_START is what I need but it works with some issues for me. Sometimes scrolling stops 1,2,3 or 4 positions earlier that I pass to `smoothScrollToPosition`. Why this problem may occur? Thanks. – Natasha Nov 02 '16 at 13:23
  • can you somehow reach your code to us ? I'm pretty sure it is your code issue. – droidev Nov 02 '16 at 13:25
  • @droidev I've been trying to reproduce the issue in demo app but no luck yet. Probably the problem really in my code or 3rd party library I use. Still working on it. I will keep you posted if there is problem with your LayoutManager. Thanks for reply anyway. – Natasha Nov 03 '16 at 10:33
  • hey @droidev can you please help m eout with this https://stackoverflow.com/questions/49952965/recyclerview-horizontal-scrolling-to-left?noredirect=1#comment87861669_49952965 –  May 19 '18 at 09:03
  • 1
    This is the correct answer despite the accepted one – Alessio Jul 12 '18 at 06:04
  • Will check that. I dint try for horizontal recycler view – droidev Dec 26 '18 at 18:18
  • @Zain Did you remember to change `VERTICAL` to `HORIZONTAL`? – Abandoned Cart May 09 '19 at 20:59
  • @AbandonedCart thanks for this tip, I solved it differently but will double check it again – Zain May 10 '19 at 11:20
  • Spent hours to do this. Best solution found on internet. Best thing is this has smooth scroll functionality. Thanks. – Dinith Rukshan Kumara Jan 10 '21 at 13:00
69

This is an extension function I wrote in Kotlin to use with the RecyclerView (based on @Paul Woitaschek answer):

fun RecyclerView.smoothSnapToPosition(position: Int, snapMode: Int = LinearSmoothScroller.SNAP_TO_START) {
  val smoothScroller = object : LinearSmoothScroller(this.context) {
    override fun getVerticalSnapPreference(): Int = snapMode
    override fun getHorizontalSnapPreference(): Int = snapMode
  }
  smoothScroller.targetPosition = position
  layoutManager?.startSmoothScroll(smoothScroller)
}

Use it like this:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
  • 34,185
  • 17
  • 113
  • 116
15

We can try like this

    recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
Ashik
  • 1,035
  • 12
  • 15
8

Override the calculateDyToMakeVisible/calculateDxToMakeVisible function in LinearSmoothScroller to implement the offset Y/X position

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
  • 81
  • 1
  • 1
  • This is what I was looking for. Thanks for sharing this for implementation the offset position of x/y :) – Zeeshan Jun 25 '19 at 22:43
8

i Used Like This :

recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
Criss
  • 755
  • 7
  • 22
7

I want to more fully address the issue of scroll duration, which, should you choose any earlier answer, will in fact will vary dramatically (and unacceptably) according to the amount of scrolling necessary to reach the target position from the current position .

To obtain a uniform scroll duration the velocity (pixels per millisecond) must account for the size of each individual item - and when the items are of non-standard dimension then a whole new level of complexity is added.

This may be why the RecyclerView developers deployed the too-hard basket for this vital aspect of smooth scrolling.

Assuming that you want a semi-uniform scroll duration, and that your list contains semi-uniform items then you will need something like this.

/** Smoothly scroll to specified position allowing for interval specification. <br>
 * Note crude deceleration towards end of scroll
 * @param rv        Your RecyclerView
 * @param toPos     Position to scroll to
 * @param duration  Approximate desired duration of scroll (ms)
 * @throws IllegalArgumentException */
private static void smoothScroll(RecyclerView rv, int toPos, int duration) throws IllegalArgumentException {
    int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;     // See androidx.recyclerview.widget.LinearSmoothScroller
    int itemHeight = rv.getChildAt(0).getHeight();  // Height of first visible view! NB: ViewGroup method!
    itemHeight = itemHeight + 33;                   // Example pixel Adjustment for decoration?
    int fvPos = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
    int i = Math.abs((fvPos - toPos) * itemHeight);
    if (i == 0) { i = (int) Math.abs(rv.getChildAt(0).getY()); }
    final int totalPix = i;                         // Best guess: Total number of pixels to scroll
    RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(rv.getContext()) {
        @Override protected int getVerticalSnapPreference() {
            return LinearSmoothScroller.SNAP_TO_START;
        }
        @Override protected int calculateTimeForScrolling(int dx) {
            int ms = (int) ( duration * dx / (float)totalPix );
            // Now double the interval for the last fling.
            if (dx < TARGET_SEEK_SCROLL_DISTANCE_PX ) { ms = ms*2; } // Crude deceleration!
            //lg(format("For dx=%d we allot %dms", dx, ms));
            return ms;
        }
    };
    //lg(format("Total pixels from = %d to %d = %d [ itemHeight=%dpix ]", fvPos, toPos, totalPix, itemHeight));
    smoothScroller.setTargetPosition(toPos);
    rv.getLayoutManager().startSmoothScroll(smoothScroller);
}

PS: I curse the day I began indiscriminately converting ListView to RecyclerView.

Bad Loser
  • 3,065
  • 1
  • 19
  • 31
  • sometimes ms returns 5000 ms which causes the view to scroll super slow, i would recommend using RecyclerView.SmoothScroller as i hinted out in my answer. https://stackoverflow.com/a/72668624/6039240 – Amr Jun 18 '22 at 10:46
2

Thanks, @droidev for the solution. If anyone looking for Kotlin solution, refer this:

    class LinearLayoutManagerWithSmoothScroller: LinearLayoutManager {
    constructor(context: Context) : this(context, VERTICAL,false)
    constructor(context: Context, orientation: Int, reverseValue: Boolean) : super(context, orientation, reverseValue)

    override fun smoothScrollToPosition(recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int) {
        super.smoothScrollToPosition(recyclerView, state, position)
        val smoothScroller = TopSnappedSmoothScroller(recyclerView?.context)
        smoothScroller.targetPosition = position
        startSmoothScroll(smoothScroller)
    }

    private class TopSnappedSmoothScroller(context: Context?) : LinearSmoothScroller(context){
        var mContext = context
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
            return LinearLayoutManagerWithSmoothScroller(mContext as Context)
                    .computeScrollVectorForPosition(targetPosition)
        }

        override fun getVerticalSnapPreference(): Int {
            return SNAP_TO_START
        }


    }

}
1

The easiest way I've found to scroll a RecyclerView is as follows:

// Define the Index we wish to scroll to.
final int lIndex = 0;
// Assign the RecyclerView's LayoutManager.
this.getRecyclerView().setLayoutManager(this.getLinearLayoutManager());
// Scroll the RecyclerView to the Index.
this.getLinearLayoutManager().smoothScrollToPosition(this.getRecyclerView(), new RecyclerView.State(), lIndex);
Mapsy
  • 4,192
  • 1
  • 37
  • 43
  • 4
    This won't scroll to a position if that position is already visible. It scrolls the least amount it needs to make the position visible. Thus why we need the one with specified offset. – TatiOverflow Aug 26 '18 at 16:36
1
  1. Extend "LinearLayout" class and override the necessary functions
  2. Create an instance of the above class in your fragment or activity
  3. Call "recyclerView.smoothScrollToPosition(targetPosition)

CustomLinearLayout.kt :

class CustomLayoutManager(private val context: Context, layoutDirection: Int):
  LinearLayoutManager(context, layoutDirection, false) {

    companion object {
      // This determines how smooth the scrolling will be
      private
      const val MILLISECONDS_PER_INCH = 300f
    }

    override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int) {

      val smoothScroller: LinearSmoothScroller = object: LinearSmoothScroller(context) {

        fun dp2px(dpValue: Float): Int {
          val scale = context.resources.displayMetrics.density
          return (dpValue * scale + 0.5f).toInt()
        }

        // change this and the return super type to "calculateDyToMakeVisible" if the layout direction is set to VERTICAL
        override fun calculateDxToMakeVisible(view: View ? , snapPreference : Int): Int {
          return super.calculateDxToMakeVisible(view, SNAP_TO_END) - dp2px(50f)
        }

        //This controls the direction in which smoothScroll looks for your view
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF ? {
          return this @CustomLayoutManager.computeScrollVectorForPosition(targetPosition)
        }

        //This returns the milliseconds it takes to scroll one pixel.
        override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
          return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
        }
      }
      smoothScroller.targetPosition = position
      startSmoothScroll(smoothScroller)
    }
  }

Note: The above example is set to HORIZONTAL direction, you can pass VERTICAL/HORIZONTAL during initialization.

If you set the direction to VERTICAL you should change the "calculateDxToMakeVisible" to "calculateDyToMakeVisible" (also mind the supertype call return value)

Activity/Fragment.kt :

...
smoothScrollerLayoutManager = CustomLayoutManager(context, LinearLayoutManager.HORIZONTAL)
recyclerView.layoutManager = smoothScrollerLayoutManager
.
.
.
fun onClick() {
  // targetPosition passed from the adapter to activity/fragment
  recyclerView.smoothScrollToPosition(targetPosition)
}
HumbleBee
  • 1,212
  • 13
  • 20
1

I have create an extension method based on position of items in a list which is bind with recycler view

Smooth scroll in large list takes longer time to scroll , use this to improve speed of scrolling and also have the smooth scroll animation. Cheers!!

fun RecyclerView?.perfectScroll(size: Int,up:Boolean = true ,smooth: Boolean = true) {
this?.apply {
    if (size > 0) {
        if (smooth) {
            val minDirectScroll = 10 // left item to scroll
            //smooth scroll
            if (size > minDirectScroll) {
                //scroll directly to certain position
                val newSize = if (up) minDirectScroll else size - minDirectScroll
                //scroll to new position
                val newPos = newSize  - 1
                //direct scroll
                scrollToPosition(newPos)
                //smooth scroll to rest
                perfectScroll(minDirectScroll, true)

            } else {
                //direct smooth scroll
                smoothScrollToPosition(if (up) 0 else size-1)
            }
        } else {
            //direct scroll
            scrollToPosition(if (up) 0 else size-1)
        }
    }
} }

Just call the method anywhere using

rvList.perfectScroll(list.size,up=true,smooth=true)
1

Here you can change to where you want to scroll, changing SNAP_TO_* return value in get**SnapPreference.

duration will be always used to scroll to the nearest item as well as the farthest item in your list.

on finish is used to do something when scrolling is almost finished.

fun RecyclerView.smoothScroll(toPos: Int, duration: Int = 500, onFinish: () -> Unit = {}) {
  try {
    val smoothScroller: RecyclerView.SmoothScroller = object : LinearSmoothScroller(context) {
        override fun getVerticalSnapPreference(): Int {
            return SNAP_TO_END
        }

        override fun calculateTimeForScrolling(dx: Int): Int {
            return duration
        }

        override fun onStop() {
            super.onStop()
            onFinish.invoke()
        }
    }
    smoothScroller.targetPosition = toPos
    layoutManager?.startSmoothScroll(smoothScroller)
  } catch (e: Exception) {
    Timber.e("FAILED TO SMOOTH SCROLL: ${e.message}")
  }
}
Amr
  • 1,068
  • 12
  • 21
0

Probably @droidev approach is the correct one, but I just want to publish something a little bit different, which does basically the same job and doesn't require extension of the LayoutManager.

A NOTE here - this is gonna work well if your item (the one that you want to scroll on the top of the list) is visible on the screen and you just want to scroll it to the top automatically. It is useful when the last item in your list has some action, which adds new items in the same list and you want to focus the user on the new added items:

int recyclerViewTop = recyclerView.getTop();
int positionTop = recyclerView.findViewHolderForAdapterPosition(positionToScroll) != null ? recyclerView.findViewHolderForAdapterPosition(positionToScroll).itemView.getTop() : 200;
final int calcOffset = positionTop - recyclerViewTop; 
//then the actual scroll is gonna happen with (x offset = 0) and (y offset = calcOffset)
recyclerView.scrollBy(0, offset);

The idea is simple: 1. We need to get the top coordinate of the recyclerview element; 2. We need to get the top coordinate of the view item that we want to scroll to the top; 3. At the end with the calculated offset we need to do

recyclerView.scrollBy(0, offset);

200 is just example hard coded integer value that you can use if the viewholder item doesn't exist, because that is possible as well.

Stoycho Andreev
  • 6,163
  • 1
  • 25
  • 27
-2

You can reverse your list by list.reverse() and finaly call RecylerView.scrollToPosition(0)

    list.reverse()
    layout = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,true)  
    RecylerView.scrollToPosition(0)
Mario
  • 1,631
  • 2
  • 21
  • 51
Thanh Vu
  • 1
  • 2