RecyclerView.Adapter.notifyItemMoved(0,1) scrolls screen
Asked Answered
E

6

15

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?

Enchantment answered 16/1, 2015 at 20:40 Comment(2)
I had the same issue with the GridLayoutManager, and the accepted answer of scrollToPosition (after the move) fixed it!Periodontics
I had issues with StaggeredGridLayoutManager, using GridLayoutManager solved the issueSupen
D
5

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.

Diastrophism answered 18/1, 2015 at 3:1 Comment(4)
Although this solved my problem, take a look at this code.google.com/p/android/issues/detail?id=99047Enchantment
Thanks for the report. We'll fix it. Sorry for the inconvenience. Luckily, there is a relatively easy workaround. Btw, scrollToPosition just brings the view to visible viewport, so it is safe to call it all the time, even if you are not moving the first item.Diastrophism
Thanks man ! I used ItemTouchHelper for dragging items and had issues when dragging first item to the second. Because of scroll. Lost 3 days figuring out what was wrong !!!Areca
I am having the same issue. I've tried calling scrollToPosition(0) on my layoutManager in the onMove() method and in the onChildDraw() method but can't get it to work. Where should I be implementing this code?Agency
M
16

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);
}
Mellins answered 4/3, 2016 at 13:43 Comment(5)
When we need to do this? After move or before move??Cogency
@Andreas Wenger, is this code supposed to be implemented in the onMove() method? I am not able to get it to work...Agency
@Agency I don't know of any onMove method in the RecyclerView or in the RecyclerView.Adapter. The solution consists of 3 steps (marked with the comments). The first section saves the current scroll position. The second section applies changes to the RecyclerView through the adapter. The adapter in the code is documented here: developer.android.com/reference/kotlin/androidx/recyclerview/… . The last section restores the scroll state that was saved in section 1.Mellins
@AndreasWenger I'm using the ItemTouchHelper class that contains the onMove method. This adds swipe to dismiss and drag and drop support to the RecyclerView. Is there another method that allows these functionalities without ItemTouchHelper?Agency
@Agency this answer's code should go wherever you are doing the swapping of two items. In your case, you are using ItemTouchHelper so that swap is happening in onMove, so it should go in onMove. Sadly I'm having the same issue as you, it doesn't work for me.Maelstrom
D
5

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.

Diastrophism answered 18/1, 2015 at 3:1 Comment(4)
Although this solved my problem, take a look at this code.google.com/p/android/issues/detail?id=99047Enchantment
Thanks for the report. We'll fix it. Sorry for the inconvenience. Luckily, there is a relatively easy workaround. Btw, scrollToPosition just brings the view to visible viewport, so it is safe to call it all the time, even if you are not moving the first item.Diastrophism
Thanks man ! I used ItemTouchHelper for dragging items and had issues when dragging first item to the second. Because of scroll. Lost 3 days figuring out what was wrong !!!Areca
I am having the same issue. I've tried calling scrollToPosition(0) on my layoutManager in the onMove() method and in the onChildDraw() method but can't get it to work. Where should I be implementing this code?Agency
P
3

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
Plasmo answered 8/1, 2020 at 11:59 Comment(0)
A
3

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)

Arielariela answered 3/3, 2021 at 19:49 Comment(0)
Y
1

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
        )
Younger answered 12/4, 2021 at 9:12 Comment(0)
K
0

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.

Kvass answered 8/6, 2020 at 10:35 Comment(1)
Hi @Bassam Helal, where do you call this method? I can't get it to work. I tried inside the onMove method where the "oldView" and "targetView" are passed as arguments...Agency

© 2022 - 2024 — McMap. All rights reserved.