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.
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);
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.
hope this helps.
smoothScrollToPosition
. Why this problem may occur? Thanks.
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)
We can try like this
recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
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)
}
i Used Like This :
recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
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.
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);
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
}
}
}
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)
Extend "LinearLayout" class and override the necessary functions Create an instance of the above class in your fragment or activity 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)
}
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.
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}")
}
}
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)
Success story sharing
protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }
. Furthermore I had to implement the abstract methodpublic PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }
.