Android recyclerview scroll to position [duplicate] - android

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.

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)
}

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}")
}
}

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.

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)

Related

How to write test case for recyclerview drag and drop using espress

I am writing test cases for recyclerview. I am stuck with drag and drop test in recyclerview. I tried in some way. But not able to write drag and drop test for recyclerview.
In recyclerview I am having drag button. Using of that button I am drag and drop the item on position to other position.
My code to swipe bottom to top
onView(withId(R.id.recyclerView)).perform(
RecyclerViewActions.actionOnItemAtPosition(0, new GeneralSwipeAction(
Swipe.SLOW, GeneralLocation.BOTTOM_CENTER, GeneralLocation.TOP_CENTER,
Press.FINGER)));
This code is swipe the recyclerview.
So I am trying like this:
onView(withId(R.id.recyclerView)).perform(
RecyclerViewActions.actionOnItemAtPosition(0,
MyViewAction.clickChildViewWithId(R.id.img_drag)),new GeneralSwipeAction(
Swipe.SLOW, GeneralLocation.TOP_RIGHT, GeneralLocation.CENTER_RIGHT,
Press.FINGER));
But this is not workout to me. Please let me any idea to test the drag and drop functionality.
The code you wrote first clicks the drag handle and then swipes from top right of the first item to its center right. If you want to drag and drop an item, you will need several custom CoordinatesProviders.
RecyclerViewCoordinatesProvider will find coordinates of an item within your RecyclerView:
class RecyclerViewCoordinatesProvider(private val position: Int, private val childItemCoordinatesProvider: CoordinatesProvider) : CoordinatesProvider {
override fun calculateCoordinates(view: View): FloatArray {
return childItemCoordinatesProvider.calculateCoordinates((view as RecyclerView).layoutManager!!.findViewByPosition(position)!!)
}
}
ChildViewCoordinatesProvider will find coordinates of a child with a given id:
class ChildViewCoordinatesProvider(private val childViewId: Int, private val insideChildViewCoordinatesProvider: CoordinatesProvider) : CoordinatesProvider {
override fun calculateCoordinates(view: View): FloatArray {
return insideChildViewCoordinatesProvider.calculateCoordinates(view.findViewById<View>(childViewId))
}
}
Lastly we will create new custom general locations, which will provide coordinates above and under a View (I assume we are reordering inside a LinearLayoutManager):
enum class CustomGeneralLocation : CoordinatesProvider {
UNDER_RIGHT {
override fun calculateCoordinates(view: View): FloatArray {
val screenLocation = IntArray(2)
view.getLocationOnScreen(screenLocation)
return floatArrayOf(screenLocation[0] + view.width - 1f, screenLocation[1] + view.height * 1.5f)
}
},
ABOVE_RIGHT {
override fun calculateCoordinates(view: View): FloatArray {
val screenLocation = IntArray(2)
view.getLocationOnScreen(screenLocation)
return floatArrayOf(screenLocation[0] + view.width - 1f, screenLocation[1] - view.height * 0.5f)
}
}
}
Now if you want to drag an item from the original position to the target position where RecyclerView's ID is server_list and the drag handle's ID is drag_handle, use the following method:
private fun reorder(from: Int, to: Int) {
onView(withId(R.id.server_list)).perform(GeneralSwipeAction(
Swipe.SLOW,
RecyclerViewCoordinatesProvider(from, ChildViewCoordinatesProvider(R.id.drag_handle, GeneralLocation.CENTER)),
RecyclerViewCoordinatesProvider(to, if (from < to) CustomGeneralLocation.UNDER_RIGHT else CustomGeneralLocation.ABOVE_RIGHT),
Press.FINGER))
}
I have a working example, although I do not know if it works with scrolling, and it throws an Exception if it fails.
The idea is to create a custom ViewAction that acts on a RecyclerView. MotionEvents are created and injected into the uicontroller. GeneralLocation, despite its name, is used to get accurate coordinates for the view holders.
I tried to use MotionEvents.sendMovement previously, but it did not work, so it appears that the interpolation is necessary in order for the phone to process the event.
class MoveViewHolder(val fromPosition: Int, val toPosition: Int) : ViewAction {
override fun getConstraints(): Matcher<View?>? {
return allOf(isAssignableFrom(RecyclerView::class.java), isDisplayed())
}
override fun getDescription(): String {
return "Long click ViewHolder #$fromPosition and swipe to position $toPosition"
}
override fun perform(uiController: UiController, view: View?) {
// For long clicking the ViewHolder
val rv = view as RecyclerView
rv.scrollToPosition(fromPosition)
val viewHolder = rv.findViewHolderForAdapterPosition(fromPosition)!!
val location = GeneralLocation.CENTER.calculateCoordinates(viewHolder.itemView)
val downEvent = MotionEvents.obtainDownEvent(
location,
floatArrayOf(0f, 0f)
)
// For moving the ViewHolder
val viewHolder2 = rv.findViewHolderForAdapterPosition(toPosition)!!
val location2 = if (toPosition > fromPosition) {
GeneralLocation.BOTTOM_CENTER.calculateCoordinates(viewHolder2.itemView)
} else {
GeneralLocation.TOP_CENTER.calculateCoordinates(viewHolder2.itemView)
}
// Interpolating the move
val longPressTimeout = (ViewConfiguration.getLongPressTimeout() * 1.5f).toLong()
var eventTime = downEvent.downTime + longPressTimeout
val durationMS = 1500
val steps = interpolate(location, location2, 10)
val intervalMS: Long = (durationMS / steps.size).toLong()
val events = mutableListOf<MotionEvent>()
for (step in steps) {
eventTime += intervalMS
events.add(MotionEvents.obtainMovement(downEvent.downTime, eventTime, step))
}
eventTime += intervalMS
events.add(
MotionEvent.obtain(
downEvent.downTime,
eventTime,
MotionEvent.ACTION_UP,
location2[0],
location2[1],
0
)
)
// For releasing the ViewHolder to its new position
val upEvent = MotionEvents.obtainUpEvent(downEvent, location2)
events.add(upEvent)
try {
uiController.injectMotionEvent(downEvent)
rv.scrollToPosition(toPosition)
uiController.loopMainThreadForAtLeast(longPressTimeout)
uiController.injectMotionEventSequence(events)
} finally {
downEvent.recycle()
for (event in events) {
event.recycle()
}
}
}
}

Recyclerview withy LinearSnapHelper - snap to next item when release finger from the screen

I have followed this Stack Overflow post to create a recyclerview with viewpager behavior.
Works fine but I need to trigger the scroll to next item when user lift the finger of the screen. Right now only snap to next item when it's 50% visible.
UPDATE WITH SOLUTION
The Crysxd response was the key. To detect the snap direction I added this code at the top of the findTargetSnapPosition function:
if (velocityY < 0)
snapToPrevious = true
else
snapToNext = true
There's no way around a custom SnapHelper. Here is a class you can copy to your project.
The solution
class ControllableSnapHelper(val onSnapped: ((Int) -> Unit)? = null) : LinearSnapHelper() {
var snappedPosition = 0
private var snapToNext = false
private var snapToPrevious = false
var recyclerView: RecyclerView? = null
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
super.attachToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
if (snapToNext) {
snapToNext = false
snappedPosition = Math.min(recyclerView?.adapter?.itemCount ?: 0, snappedPosition + 1)
} else if (snapToPrevious) {
snapToPrevious = false
snappedPosition = Math.max(0, snappedPosition - 1)
} else {
snappedPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
}
onSnapped?.invoke(snappedPosition)
return snappedPosition
}
fun snapToNext() {
snapToNext = true
onFling(Int.MAX_VALUE, Int.MAX_VALUE)
}
fun snapToPrevious() {
snapToPrevious = true
onFling(Int.MAX_VALUE, Int.MAX_VALUE)
}
}
You can replace LinearSnapHelper with PagerSnapHelper as well. Simply call snapToNext() or snapToPrevious(). I also implemented a callback which is passed in the constructor.
How it works
We simply overwrite the findTargetSnapPosition(...) method. This method can return any position and the SnapHelper implementation will then compute the scroll animation to get there. The functions snapToNext() and snapToPrevious() simply set a boolean which tells us to snap to the next or previous view the next time findTargetSnapPosition(...) is called. It would also be very simple to add a snapTo(index: Int) function. After setting the boolean, we need to start the snap progress. To do so, we call onFling(...) which would be called when the user moved the RecyclerView. We need to pass in a value which exceeds the minimum velocity set by SnapHelper, so we just use Int.MAX_VALUE. As we never call super.findTargetSnapPosition(...), the actual velocity used by PagerSnapHelper doesn't matter.

How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?

Background
It's possible to snap a RecyclerView to its center using :
LinearSnapHelper().attachToRecyclerView(recyclerView)
Example:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
val cellSize = recyclerView.width / 3
view.layoutParams.height = cellSize
view.layoutParams.width = cellSize
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {}
}
}
LinearSnapHelper().attachToRecyclerView(recyclerView)
}
}
activity_main.xml
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
It's also possible to snap it to other sides, as was done in some libraries, such as here.
There are also libraries that allow to have a RecyclerView that can work like a ViewPager, such as here.
The problem
Supposed I have a RecyclerView (horizontal in my case) with many items, and I want that it will treat every X items (X is constant) as a single unit, and snap to each of those units.
For example, if I scroll a bit, it could snap to either the 0-item, or the X-item, but not to something in between them.
In a way, it's similar in its behavior to a case of a normal ViewPager, just that each page would have X items in it.
For example, if we continue from the sample code I wrote above,suppose X==3 , the snapping would be from this idle state:
to this idle state (in case we scrolled enough, otherwise would stay in previous state) :
Flinging or scrolling more should be handled like on ViewPager, just like the library I've mentioned above.
Scrolling more (in the same direction) to the next snapping point would be to reach item "6" , "9", and so on...
What I tried
I tried to search for alternative libraries, and I also tried to read the docs regarding this, but I didn't find anything that might be useful.
It might also be possible by using a ViewPager, but I think that's not the best way, because ViewPager doesn't recycle its items well, and I think it's less flexible than RecyclerView in terms of how to snap.
The questions
Is it possible to set RecyclerView to snap every X items, to treat each X items as a single page to snap to?
Of course, the items will take enough space for the whole RecyclerView, evenly.
Supposed it is possible, how would I get a callback when the RecyclerView is about to snap to a certain item, including having this item, before it got snapped? I ask this because it's related to the same question I asked here.
Kotlin solution
A working Kotlin solution based on "Cheticamp" answer (here), without the need to verify that you have the RecyclerView size, and with the choice of having a grid instead of a list, in the sample:
MainActivity.kt
class MainActivity : AppCompatActivity() {
val USE_GRID = false
// val USE_GRID = true
val ITEMS_PER_PAGE = 4
var selectedItemPos = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = if (selectedItemPos == position) "selected: $position" else position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
view.layoutParams.width = if (USE_GRID)
recyclerView.width / (ITEMS_PER_PAGE / 2)
else
recyclerView.width / 4
view.layoutParams.height = recyclerView.height / (ITEMS_PER_PAGE / 2)
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {
}
}
}
recyclerView.layoutManager = if (USE_GRID)
GridLayoutManager(this, ITEMS_PER_PAGE / 2, GridLayoutManager.HORIZONTAL, false)
else
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
val snapToBlock = SnapToBlock(recyclerView, ITEMS_PER_PAGE)
snapToBlock.attachToRecyclerView(recyclerView)
snapToBlock.setSnapBlockCallback(object : SnapToBlock.SnapBlockCallback {
override fun onBlockSnap(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
override fun onBlockSnapped(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
})
}
}
SnapToBlock.kt
/**#param maxFlingBlocks Maxim blocks to move during most vigorous fling*/
class SnapToBlock constructor(private val maxFlingBlocks: Int) : SnapHelper() {
private var recyclerView: RecyclerView? = null
// Total number of items in a block of view in the RecyclerView
private var blocksize: Int = 0
// Maximum number of positions to move on a fling.
private var maxPositionsToMove: Int = 0
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private var itemDimension: Int = 0
// Callback interface when blocks are snapped.
private var snapBlockCallback: SnapBlockCallback? = null
// When snapping, used to determine direction of snap.
private var priorFirstPosition = RecyclerView.NO_POSITION
// Our private scroller
private var scroller: Scroller? = null
// Horizontal/vertical layout helper
private var orientationHelper: OrientationHelper? = null
// LTR/RTL helper
private var layoutDirectionHelper: LayoutDirectionHelper? = null
#Throws(IllegalStateException::class)
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView != null) {
this.recyclerView = recyclerView
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
orientationHelper = when {
layoutManager.canScrollHorizontally() -> OrientationHelper.createHorizontalHelper(layoutManager)
layoutManager.canScrollVertically() -> OrientationHelper.createVerticalHelper(layoutManager)
else -> throw IllegalStateException("RecyclerView must be scrollable")
}
scroller = Scroller(this.recyclerView!!.context, sInterpolator)
initItemDimensionIfNeeded(layoutManager)
}
super.attachToRecyclerView(recyclerView)
}
// Called when the target view is available and we need to know how much more
// to scroll to get it lined up with the side of the RecyclerView.
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray {
val out = IntArray(2)
initLayoutDirectionHelperIfNeeded(layoutManager)
if (layoutManager.canScrollHorizontally())
out[0] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (layoutManager.canScrollVertically())
out[1] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (snapBlockCallback != null)
if (out[0] == 0 && out[1] == 0)
snapBlockCallback!!.onBlockSnapped(layoutManager.getPosition(targetView))
else
snapBlockCallback!!.onBlockSnap(layoutManager.getPosition(targetView))
return out
}
private fun initLayoutDirectionHelperIfNeeded(layoutManager: RecyclerView.LayoutManager) {
if (layoutDirectionHelper == null)
if (layoutManager.canScrollHorizontally())
layoutDirectionHelper = LayoutDirectionHelper()
else if (layoutManager.canScrollVertically())
// RTL doesn't matter for vertical scrolling for this class.
layoutDirectionHelper = LayoutDirectionHelper(false)
}
// We are flinging and need to know where we are heading.
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
initLayoutDirectionHelperIfNeeded(layoutManager)
val lm = layoutManager as LinearLayoutManager
initItemDimensionIfNeeded(layoutManager)
scroller!!.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE)
return when {
velocityX != 0 -> layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
else -> if (velocityY != 0)
layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalY, itemDimension)
else RecyclerView.NO_POSITION
}
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
// Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
// or, 2) toward the top of the data and may be off-screen.
val snapPos = calcTargetPosition(layoutManager as LinearLayoutManager)
val snapView = if (snapPos == RecyclerView.NO_POSITION)
null
else
layoutManager.findViewByPosition(snapPos)
if (snapView == null)
Log.d(TAG, "<<<<findSnapView is returning null!")
Log.d(TAG, "<<<<findSnapView snapos=" + snapPos)
return snapView
}
// Does the heavy lifting for findSnapView.
private fun calcTargetPosition(layoutManager: LinearLayoutManager): Int {
val snapPos: Int
initLayoutDirectionHelperIfNeeded(layoutManager)
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
if (firstVisiblePos == RecyclerView.NO_POSITION)
return RecyclerView.NO_POSITION
initItemDimensionIfNeeded(layoutManager)
if (firstVisiblePos >= priorFirstPosition) {
// Scrolling toward bottom of data
val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION && firstCompletePosition % blocksize == 0)
firstCompletePosition
else
roundDownToBlockSize(firstVisiblePos + blocksize)
} else {
// Scrolling toward top of data
snapPos = roundDownToBlockSize(firstVisiblePos)
// Check to see if target view exists. If it doesn't, force a smooth scroll.
// SnapHelper only snaps to existing views and will not scroll to a non-existant one.
// If limiting fling to single block, then the following is not needed since the
// views are likely to be in the RecyclerView pool.
if (layoutManager.findViewByPosition(snapPos) == null) {
val toScroll = layoutDirectionHelper!!.calculateDistanceToScroll(layoutManager, snapPos)
recyclerView!!.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator)
}
}
priorFirstPosition = firstVisiblePos
return snapPos
}
private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) {
if (itemDimension != 0)
return
val child = layoutManager.getChildAt(0) ?: return
if (layoutManager.canScrollHorizontally()) {
itemDimension = child.width
blocksize = getSpanCount(layoutManager) * (recyclerView!!.width / itemDimension)
} else if (layoutManager.canScrollVertically()) {
itemDimension = child.height
blocksize = getSpanCount(layoutManager) * (recyclerView!!.height / itemDimension)
}
maxPositionsToMove = blocksize * maxFlingBlocks
}
private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int = (layoutManager as? GridLayoutManager)?.spanCount ?: 1
private fun roundDownToBlockSize(trialPosition: Int): Int = trialPosition - trialPosition % blocksize
private fun roundUpToBlockSize(trialPosition: Int): Int = roundDownToBlockSize(trialPosition + blocksize - 1)
override fun createScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? {
return if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider)
null
else object : LinearSmoothScroller(recyclerView!!.context) {
override fun onTargetFound(targetView: View, state: RecyclerView.State?, action: RecyclerView.SmoothScroller.Action) {
val snapDistances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager, targetView)
val dx = snapDistances[0]
val dy = snapDistances[1]
val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)))
if (time > 0)
action.update(dx, dy, time, sInterpolator)
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
fun setSnapBlockCallback(callback: SnapBlockCallback?) {
snapBlockCallback = callback
}
/*
Helper class that handles calculations for LTR and RTL layouts.
*/
private inner class LayoutDirectionHelper {
// Is the layout an RTL one?
private val mIsRTL: Boolean
constructor() {
mIsRTL = ViewCompat.getLayoutDirection(recyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL
}
constructor(isRTL: Boolean) {
mIsRTL = isRTL
}
/*
Calculate the amount of scroll needed to align the target view with the layout edge.
*/
fun getScrollToAlignView(targetView: View): Int = if (mIsRTL)
orientationHelper!!.getDecoratedEnd(targetView) - recyclerView!!.width
else
orientationHelper!!.getDecoratedStart(targetView)
/**
* Calculate the distance to final snap position when the view corresponding to the snap
* position is not currently available.
*
* #param layoutManager LinearLayoutManager or descendent class
* #param targetPos - Adapter position to snap to
* #return int[2] {x-distance in pixels, y-distance in pixels}
*/
fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray {
val out = IntArray(2)
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
if (layoutManager.canScrollHorizontally()) {
if (targetPos <= firstVisiblePos) // scrolling toward top of data
if (mIsRTL) {
val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition())
out[0] = orientationHelper!!.getDecoratedEnd(lastView) + (firstVisiblePos - targetPos) * itemDimension
} else {
val firstView = layoutManager.findViewByPosition(firstVisiblePos)
out[0] = orientationHelper!!.getDecoratedStart(firstView) - (firstVisiblePos - targetPos) * itemDimension
}
}
if (layoutManager.canScrollVertically() && targetPos <= firstVisiblePos) { // scrolling toward top of data
val firstView = layoutManager.findViewByPosition(firstVisiblePos)
out[1] = firstView.top - (firstVisiblePos - targetPos) * itemDimension
}
return out
}
/*
Calculate the number of positions to move in the RecyclerView given a scroll amount
and the size of the items to be scrolled. Return integral multiple of mBlockSize not
equal to zero.
*/
fun getPositionsToMove(llm: LinearLayoutManager, scroll: Int, itemSize: Int): Int {
var positionsToMove: Int
positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize)
if (positionsToMove < blocksize)
// Must move at least one block
positionsToMove = blocksize
else if (positionsToMove > maxPositionsToMove)
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = maxPositionsToMove
if (scroll < 0)
positionsToMove *= -1
if (mIsRTL)
positionsToMove *= -1
return if (layoutDirectionHelper!!.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove
} else roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove
// Scrolling toward the top of the data.
}
fun isDirectionToBottom(velocityNegative: Boolean): Boolean = if (mIsRTL) velocityNegative else !velocityNegative
}
interface SnapBlockCallback {
fun onBlockSnap(snapPosition: Int)
fun onBlockSnapped(snapPosition: Int)
}
companion object {
// Borrowed from ViewPager.java
private val sInterpolator = Interpolator { input ->
var t = input
// _o(t) = t * t * ((tension + 1) * t + tension)
// o(t) = _o(t - 1) + 1
t -= 1.0f
t * t * t + 1.0f
}
private val MILLISECONDS_PER_INCH = 100f
private val TAG = "SnapToBlock"
}
}
Update
Even though I've marked an answer as accepted, as it works fine, I've noticed it has serious issues:
Smooth scrolling doesn't seem to work fine (doesn't scroll to correct place). Only scrolling that work is as such (but with the "smearing" effect) :
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
When switching to RTL (Right to left) locale such as Hebrew ("עברית"), it doesn't let me scroll at all.
I've noticed that onCreateViewHolder is called a lot. In fact it is called every time I scroll, even for times it should have recycled the ViewHolders. This means there is an excessive creation of views, and it might also mean there is a memory leak.
I've tried to fix those myself, but failed so far.
If anyone here knows how to fix it, I will grant the extra, new bounty
Update: as we got a fix for RTL/LTR, I've updated the Kotlin solution within this post.
Update: about point #3 , this seems to be because there is a pool of views for the recyclerView, which gets filled too soon. To handle this, we can simply enlarge the pool size, by using recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) for each view type we have in it. Weird thing that this is really needed. I've posted about it to Google (here and here) but was rejected that the pool should be unlimited by default. In the end, I decided to at least request to have a more convinient function to do it for all view types (here).
SnapHelper supplies the necessary framework for what you are attempting, but it needs to be extended to handle blocks of views. The class SnapToBlock below extends SnapHelper to snap to blocks of views. In the example, I have used four views to a block but it can be more or less.
Update: The code has been change to accommodate GridLayoutManager as well as LinearLayoutManager. Flinging is now inhibited so the snapping works more list a ViewPager. Horizontal and vertical scrolling is now supported as well as LTR and RTL layouts.
Update: Changed smooth scroll interpolator to be more like ViewPager.
Update: Adding callbacks for pre/post snapping.
Update: Adding support for RTL layouts.
Here is a quick video of the sample app:
Set up the layout manager as follows:
// For LinearLayoutManager horizontal orientation
recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
// For GridLayoutManager vertical orientation
recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, RecyclerView.VERTICAL, false));
Add the following to attach the SnapToBlock to the RecyclerView.
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);
snapToBlock.attachToRecyclerView(recyclerView);
mMaxFlingPages is the maximum number of blocks (rowsCols * spans) to allow to be flung at one time.
For call backs when a snap is about to be made and has been completed, add the following:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() {
#Override
public void onBlockSnap(int snapPosition) {
...
}
#Override
public void onBlockSnapped(int snapPosition) {
...
}
});
SnapToBlock.java
/* The number of items in the RecyclerView should be a multiple of block size; otherwise, the
extra item views will not be positioned on a block boundary when the end of the data is reached.
Pad out with empty item views if needed.
Updated to accommodate RTL layouts.
*/
public class SnapToBlock extends SnapHelper {
private RecyclerView mRecyclerView;
// Total number of items in a block of view in the RecyclerView
private int mBlocksize;
// Maximum number of positions to move on a fling.
private int mMaxPositionsToMove;
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private int mItemDimension;
// Maxim blocks to move during most vigorous fling.
private final int mMaxFlingBlocks;
// Callback interface when blocks are snapped.
private SnapBlockCallback mSnapBlockCallback;
// When snapping, used to determine direction of snap.
private int mPriorFirstPosition = RecyclerView.NO_POSITION;
// Our private scroller
private Scroller mScroller;
// Horizontal/vertical layout helper
private OrientationHelper mOrientationHelper;
// LTR/RTL helper
private LayoutDirectionHelper mLayoutDirectionHelper;
// Borrowed from ViewPager.java
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
// _o(t) = t * t * ((tension + 1) * t + tension)
// o(t) = _o(t - 1) + 1
t -= 1.0f;
return t * t * t + 1.0f;
}
};
SnapToBlock(int maxFlingBlocks) {
super();
mMaxFlingBlocks = maxFlingBlocks;
}
#Override
public void attachToRecyclerView(#Nullable final RecyclerView recyclerView)
throws IllegalStateException {
if (recyclerView != null) {
mRecyclerView = recyclerView;
final LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager.canScrollHorizontally()) {
mOrientationHelper = OrientationHelper.createHorizontalHelper(layoutManager);
mLayoutDirectionHelper =
new LayoutDirectionHelper(ViewCompat.getLayoutDirection(mRecyclerView));
} else if (layoutManager.canScrollVertically()) {
mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
// RTL doesn't matter for vertical scrolling for this class.
mLayoutDirectionHelper = new LayoutDirectionHelper(RecyclerView.LAYOUT_DIRECTION_LTR);
} else {
throw new IllegalStateException("RecyclerView must be scrollable");
}
mScroller = new Scroller(mRecyclerView.getContext(), sInterpolator);
initItemDimensionIfNeeded(layoutManager);
}
super.attachToRecyclerView(recyclerView);
}
// Called when the target view is available and we need to know how much more
// to scroll to get it lined up with the side of the RecyclerView.
#NonNull
#Override
public int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (layoutManager.canScrollVertically()) {
out[1] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (mSnapBlockCallback != null) {
if (out[0] == 0 && out[1] == 0) {
mSnapBlockCallback.onBlockSnapped(layoutManager.getPosition(targetView));
} else {
mSnapBlockCallback.onBlockSnap(layoutManager.getPosition(targetView));
}
}
return out;
}
// We are flinging and need to know where we are heading.
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
int velocityX, int velocityY) {
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
initItemDimensionIfNeeded(layoutManager);
mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MAX_VALUE);
if (velocityX != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalX(), mItemDimension);
}
if (velocityY != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalY(), mItemDimension);
}
return RecyclerView.NO_POSITION;
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
#Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
// or, 2) toward the top of the data and may be off-screen.
int snapPos = calcTargetPosition((LinearLayoutManager) layoutManager);
View snapView = (snapPos == RecyclerView.NO_POSITION)
? null : layoutManager.findViewByPosition(snapPos);
if (snapView == null) {
Log.d(TAG, "<<<<findSnapView is returning null!");
}
Log.d(TAG, "<<<<findSnapView snapos=" + snapPos);
return snapView;
}
// Does the heavy lifting for findSnapView.
private int calcTargetPosition(LinearLayoutManager layoutManager) {
int snapPos;
int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (firstVisiblePos == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
initItemDimensionIfNeeded(layoutManager);
if (firstVisiblePos >= mPriorFirstPosition) {
// Scrolling toward bottom of data
int firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstCompletePosition != RecyclerView.NO_POSITION
&& firstCompletePosition % mBlocksize == 0) {
snapPos = firstCompletePosition;
} else {
snapPos = roundDownToBlockSize(firstVisiblePos + mBlocksize);
}
} else {
// Scrolling toward top of data
snapPos = roundDownToBlockSize(firstVisiblePos);
// Check to see if target view exists. If it doesn't, force a smooth scroll.
// SnapHelper only snaps to existing views and will not scroll to a non-existant one.
// If limiting fling to single block, then the following is not needed since the
// views are likely to be in the RecyclerView pool.
if (layoutManager.findViewByPosition(snapPos) == null) {
int[] toScroll = mLayoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos);
mRecyclerView.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator);
}
}
mPriorFirstPosition = firstVisiblePos;
return snapPos;
}
private void initItemDimensionIfNeeded(final RecyclerView.LayoutManager layoutManager) {
if (mItemDimension != 0) {
return;
}
View child;
if ((child = layoutManager.getChildAt(0)) == null) {
return;
}
if (layoutManager.canScrollHorizontally()) {
mItemDimension = child.getWidth();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getWidth() / mItemDimension);
} else if (layoutManager.canScrollVertically()) {
mItemDimension = child.getHeight();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getHeight() / mItemDimension);
}
mMaxPositionsToMove = mBlocksize * mMaxFlingBlocks;
}
private int getSpanCount(RecyclerView.LayoutManager layoutManager) {
return (layoutManager instanceof GridLayoutManager)
? ((GridLayoutManager) layoutManager).getSpanCount()
: 1;
}
private int roundDownToBlockSize(int trialPosition) {
return trialPosition - trialPosition % mBlocksize;
}
private int roundUpToBlockSize(int trialPosition) {
return roundDownToBlockSize(trialPosition + mBlocksize - 1);
}
#Nullable
protected LinearSmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
#Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, sInterpolator);
}
}
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
public void setSnapBlockCallback(#Nullable SnapBlockCallback callback) {
mSnapBlockCallback = callback;
}
/*
Helper class that handles calculations for LTR and RTL layouts.
*/
private class LayoutDirectionHelper {
// Is the layout an RTL one?
private final boolean mIsRTL;
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
LayoutDirectionHelper(int direction) {
mIsRTL = direction == View.LAYOUT_DIRECTION_RTL;
}
/*
Calculate the amount of scroll needed to align the target view with the layout edge.
*/
int getScrollToAlignView(View targetView) {
return (mIsRTL)
? mOrientationHelper.getDecoratedEnd(targetView) - mRecyclerView.getWidth()
: mOrientationHelper.getDecoratedStart(targetView);
}
/**
* Calculate the distance to final snap position when the view corresponding to the snap
* position is not currently available.
*
* #param layoutManager LinearLayoutManager or descendent class
* #param targetPos - Adapter position to snap to
* #return int[2] {x-distance in pixels, y-distance in pixels}
*/
int[] calculateDistanceToScroll(LinearLayoutManager layoutManager, int targetPos) {
int[] out = new int[2];
int firstVisiblePos;
firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (layoutManager.canScrollHorizontally()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
if (mIsRTL) {
View lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition());
out[0] = mOrientationHelper.getDecoratedEnd(lastView)
+ (firstVisiblePos - targetPos) * mItemDimension;
} else {
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[0] = mOrientationHelper.getDecoratedStart(firstView)
- (firstVisiblePos - targetPos) * mItemDimension;
}
}
}
if (layoutManager.canScrollVertically()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[1] = firstView.getTop() - (firstVisiblePos - targetPos) * mItemDimension;
}
}
return out;
}
/*
Calculate the number of positions to move in the RecyclerView given a scroll amount
and the size of the items to be scrolled. Return integral multiple of mBlockSize not
equal to zero.
*/
int getPositionsToMove(LinearLayoutManager llm, int scroll, int itemSize) {
int positionsToMove;
positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize);
if (positionsToMove < mBlocksize) {
// Must move at least one block
positionsToMove = mBlocksize;
} else if (positionsToMove > mMaxPositionsToMove) {
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = mMaxPositionsToMove;
}
if (scroll < 0) {
positionsToMove *= -1;
}
if (mIsRTL) {
positionsToMove *= -1;
}
if (mLayoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
return roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove;
}
// Scrolling toward the top of the data.
return roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove;
}
boolean isDirectionToBottom(boolean velocityNegative) {
//noinspection SimplifiableConditionalExpression
return mIsRTL ? velocityNegative : !velocityNegative;
}
}
public interface SnapBlockCallback {
void onBlockSnap(int snapPosition);
void onBlockSnapped(int snapPosition);
}
private static final float MILLISECONDS_PER_INCH = 100f;
#SuppressWarnings("unused")
private static final String TAG = "SnapToBlock";
}
The SnapBlockCallback interface defined above can be used to report the adapter position of the view at the start of the block to be snapped. The view associated with that position may not be instantiated when the call is made if the view is off screen.
This library it is useful https://github.com/TakuSemba/MultiSnapRecyclerView
//Adding multisnap to the recyclerview
val multiSnapHelper = MultiSnapHelper(MultiSnapHelper.DEFAULT_GRAVITY, 1, 200F)
multiSnapHelper.attachToRecyclerView(recyclerView)
this code above is for your activity via code
<com.takusemba.multisnaprecyclerview.MultiSnapRecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:msrv_gravity="start" or center, end
app:msrv_interval="2" items to scroll over
app:msrv_ms_per_inch="100" /> // speed of scrolling through.
and this is the same way but in xml it is your choice
all this information its from the documentation
I would do something like that
Block scrolling inside RecyclerView (e.g How to disable RecyclerView scrolling?)
Create Gesture Fling Detecor and attach it to RecyclerView
Inside Gesture Detector detect fling events events
On Fling event, detect side (left right)
Scroll RecyclerView to position (first Visible item + your const * (left?-1:1))
should work :)

RecyclerView - How to smooth scroll to top of item on a certain position?

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.
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)
}
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}")
}
}
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.
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)

RecyclerView.Adapter.notifyItemMoved(0,1) scrolls screen

I have a RecyclerView managed by a LinearlayoutManager, if I swap item 1 with 0 and then call mAdapter.notifyItemMoved(0,1), the moving animation causes the screen to scroll. How can I prevent it?
Sadly the workaround presented by yigit scrolls the RecyclerView to the top. This is the best workaround I found till now:
// figure out the position of the first visible item
int firstPos = manager.findFirstCompletelyVisibleItemPosition();
int offsetTop = 0;
if(firstPos >= 0) {
View firstView = manager.findViewByPosition(firstPos);
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView);
}
// apply changes
adapter.notify...
// reapply the saved position
if(firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop);
}
Call scrollToPosition(0) after moving items. Unfortunately, i assume, LinearLayoutManager tries to keep first item stable, which moves so it moves the list with it.
Translate #Andreas Wenger's answer to kotlin:
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
var offsetTop = 0
if (firstPos >= 0) {
val firstView = manager.findViewByPosition(firstPos)!!
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView)
}
// apply changes
adapter.notify...
if (firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop)
}
In my case, the view can have a top margin, which also needs to be counted in the offset, otherwise the recyclerview will not scroll to the intended position. To do so, just write:
val topMargin = (firstView.layoutParams as? MarginLayoutParams)?.topMargin ?: 0
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - topMargin
Even easier if you have ktx dependency in your project:
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - firstView.marginTop
I've faced the same problem. Nothing of the suggested helped. Each solution fix and breakes different cases.
But this workaround worked for me:
adapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if (fromPosition == 0 || toPosition == 0)
binding.recycler.scrollToPosition(0)
}
})
It helps to prevent scrolling while moving the first item for cases: direct notifyItemMoved and via ItemTouchHelper (drag and drop)
I have faced the same problem. In my case, the scroll happens on the first visible item (not only on the first item in the dataset). And I would like to thanks everybody because their answers help me to solve this problem.
I inspire my solution based on Andreas Wenger' answer and from resoluti0n' answer
And, here is my solution (in Kotlin):
RecyclerViewOnDragFistItemScrollSuppressor.kt
class RecyclerViewOnDragFistItemScrollSuppressor private constructor(
lifecycleOwner: LifecycleOwner,
private val recyclerView: RecyclerView
) : LifecycleObserver {
private val adapterDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
suppressScrollIfNeeded(fromPosition, toPosition)
}
}
init {
lifecycleOwner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun registerAdapterDataObserver() {
recyclerView.adapter?.registerAdapterDataObserver(adapterDataObserver) ?: return
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun unregisterAdapterDataObserver() {
recyclerView.adapter?.unregisterAdapterDataObserver(adapterDataObserver) ?: return
}
private fun suppressScrollIfNeeded(fromPosition: Int, toPosition: Int) {
(recyclerView.layoutManager as LinearLayoutManager).apply {
var scrollPosition = -1
if (isFirstVisibleItem(fromPosition)) {
scrollPosition = fromPosition
} else if (isFirstVisibleItem(toPosition)) {
scrollPosition = toPosition
}
if (scrollPosition == -1) return
scrollToPositionWithCalculatedOffset(scrollPosition)
}
}
companion object {
fun observe(
lifecycleOwner: LifecycleOwner,
recyclerView: RecyclerView
): RecyclerViewOnDragFistItemScrollSuppressor {
return RecyclerViewOnDragFistItemScrollSuppressor(lifecycleOwner, recyclerView)
}
}
}
private fun LinearLayoutManager.isFirstVisibleItem(position: Int): Boolean {
apply {
return position == findFirstVisibleItemPosition()
}
}
private fun LinearLayoutManager.scrollToPositionWithCalculatedOffset(position: Int) {
apply {
val offset = findViewByPosition(position)?.let {
getDecoratedTop(it) - getTopDecorationHeight(it)
} ?: 0
scrollToPositionWithOffset(position, offset)
}
}
and then, you may use it as (e.g. fragment):
RecyclerViewOnDragFistItemScrollSuppressor.observe(
viewLifecycleOwner,
binding.recyclerView
)
LinearLayoutManager has done this for you in LinearLayoutManager.prepareForDrop.
All you need to provide is the moving (old) View and the target (new) View.
layoutManager.prepareForDrop(oldView, targetView, -1, -1)
// the numbers, x and y don't matter to LinearLayoutManager's implementation of prepareForDrop
It's an "unofficial" API because it states in the source
// This method is only intended to be called (and should only ever be called) by
// ItemTouchHelper.
public void prepareForDrop(#NonNull View view, #NonNull View target, int x, int y) {
...
}
But it still works and does exactly what the other answers say, doing all the offset calculations accounting for layout direction for you.
This is actually the same method that is called by LinearLayoutManager when used by an ItemTouchHelper to account for this dreadful bug.

Categories

Resources