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.
Related
I already tried this solution here, but unfortunately doesnt work in my scenario.
Ill keep it simple: I have multiple viewHolders with multiple animations for a chat App,
Since I have no touch listeners to register the adapter position of the typing indicators, I have:
In my CustomAdapter
private var typingIndicatorAdapterPosition: Int = -1
private var inlineErrorAdapterPosition: Int = -1
In my onBindViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
...
USER_REQUEST_TEXT -> {
val userRequestViewHolder = (holder as UserRequestViewHolder)
configUserRequestViewHolder(userRequestViewHolder, position)
userRequestViewHolder.setIsRecyclable(false)
}
TYPE_INDICATOR -> {
val typingIndicatorViewHolder = (holder as TypingIndicatorViewHolder)
configTypingIndicatorViewHolder(typingIndicatorViewHolder, position)
typingIndicatorAdapterPosition = typingIndicatorViewHolder.layoutPosition
typingIndicatorViewHolder.setIsRecyclable(true)
}
INLINE_ERROR -> {
val inlineErrorViewHolder = (holder as InlineErrorViewHolder)
configInlineErrorViewHolder(inlineErrorViewHolder, position)
inlineErrorAdapterPosition = inlineErrorViewHolder.layoutPosition
inlineErrorViewHolder.setIsRecyclable(true)
}
}
}
my adapter code for deletion :
fun removeTypingIndicator() {
if(typingIndicatorAdapterPosition > 0) {
if(messageContainerList[typingIndicatorAdapterPosition].messageType == TYPE_INDICATOR) {
messageContainerList.removeAt(typingIndicatorAdapterPosition)
notifyItemRemoved(typingIndicatorAdapterPosition)
notifyItemRangeChanged(typingIndicatorAdapterPosition, itemCount - 1)
typingIndicatorAdapterPosition = -1
}
}
}
Note - I do not prefer notifyDataSetChanged() etc. as it cancels the animations.
here are some screen shots:
This line looks incorrect:
notifyItemRangeChanged(typingIndicatorAdapterPosition, itemCount - 1)
Assuming you're trying to change all the items from the typing indicator position through the end of the list, it looks like you're trying to use it as (start, end), but the parameters are actually (start, itemCount), with itemCount being the number of items changed at that index. See the documentation here for more information.
I'm working on a grid interface (using VerticalGridSupportFragment) for Android TV and I'm looking for a way to allow users to move around items in the grid.
The idea is that the grid contains a number of TV channels and that the users should be able to change the order of the TV channels (sort / reorder). My proposed solution is to select a channel by clicking it. The channel then becomes "sticky" allowing you to move it around. When you're happy with the position, you click on the channel again, confirming its new position.
The obvious solution is to do something along the lines of this:
getVerticalGridView()?.let {
it.setOnChildSelectedListener { _, _, position, _ ->
// Move the item in the previous position to the new position
adapter.move(oldPosition, position)
// Set old position to currently selected position.
oldPosition = position
}
}
fun VerticalGridSupportFragment.getVerticalGridView(): VerticalGridView? {
return VerticalGridSupportFragment::class.java.getDeclaredField("mGridViewHolder")?.let {
it.isAccessible = true
return (it.get(this) as VerticalGridPresenter.ViewHolder).gridView
}
}
The problem with this is that adapter.move() causes another child selected event.
I've tried to circumvent this issue by temporarily removing the selection listener and instead keep a ObjectAdapter.DataObserver to notify me of onItemMoved() events, in which I set the selected position and once again set a selection listener.
This doesn't seem to work fully either.
It's not possible to use ItemTouchHelper as that was designed for touch purposes and not using a remote like we do on Android TV.
The official Android TV launcher app is doing something similar to what I need when you rearrange app shortcuts on the homescreen, but I can't think of a way to make it work.
Found a solution, which also appears to be what Google is using for the Android TV launcher.
In short: Create a custom VerticalGridView and override its focusSearch() method to determine how move / swap items.
Something similar to this:
class EditableVerticalGridView #JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0) :
VerticalGridView(context, attrs, defStyle) {
override fun focusSearch(focused: View, direction: Int): View {
return if (focused.isSelected) {
swapItemsIfNeeded(focused, direction)
} else super.focusSearch(focused, direction)
}
private fun swapItemsIfNeeded(focused: View, direction: Int): View {
val position = getChildAdapterPosition(focused)
if (!itemAnimator.isRunning) {
if (canMoveInDirection(position, direction)) {
when (direction) {
FOCUS_LEFT -> moveChannel(position, position - 1)
FOCUS_UP -> moveChannel(position, position - NUM_COLUMN)
FOCUS_RIGHT -> moveChannel(position, position + 1)
FOCUS_DOWN -> moveChannel(position, position + NUM_COLUMN)
}
}
}
return focused
}
private fun canMoveInDirection(position: Int, direction: Int): Boolean {
when (direction) {
FOCUS_LEFT -> {
return position % NUM_COLUMN > 0
}
FOCUS_UP -> {
return position - NUM_COLUMN >= 0
}
FOCUS_RIGHT -> {
return !(position % NUM_COLUMN >= (NUM_COLUMN - 1) ||
position >= adapter.itemCount - 1)
}
FOCUS_DOWN -> {
return position + NUM_COLUMN <= adapter.itemCount - 1
}
else -> {
return false
}
}
}
private fun moveChannel(fromPosition: Int, toPosition: Int) {
(adapter as AllowedChannelAdapter).moveChannel(fromPosition, toPosition)
}
companion object {
private const val NUM_COLUMN: Int = 6
}
}
... and the moveChannel() function:
fun moveChannel(from: Int, to: Int) {
var offset = 1
if (from >= 0 && from <= channelItems.size - 1 && to >= 0 && to <= channelItems.size - 1) {
val fromItem = channelItems[from]
channelItems[from] = channelItems[to]
channelItems[to] = fromItem
notifyItemMoved(from, to)
val positionDifference = to - from
if (Math.abs(positionDifference) > 1) {
if (positionDifference > 0) {
offset = -1
}
notifyItemMoved(to + offset, from)
}
}
}
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)
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.
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)