RecyclerView.Adapter.notifyItemMoved(0,1) 滚动屏幕

Posted

技术标签:

【中文标题】RecyclerView.Adapter.notifyItemMoved(0,1) 滚动屏幕【英文标题】:RecyclerView.Adapter.notifyItemMoved(0,1) scrolls screen 【发布时间】:2015-03-15 13:23:06 【问题描述】:

我有一个由LinearlayoutManager 管理的RecyclerView,如果我用0 交换项目1 然后调用mAdapter.notifyItemMoved(0,1),移动动画会导致屏幕滚动。如何预防?

【问题讨论】:

我在 GridLayoutManager 上遇到了同样的问题,scrollToPosition 的接受答案(在移动之后)修复了它! 我遇到了StaggeredGridLayoutManager 的问题,使用GridLayoutManager 解决了这个问题 【参考方案1】:

遗憾的是,yigit 提出的解决方法将RecyclerView 滚动到顶部。这是迄今为止我发现的最好的解决方法:

// 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);

【讨论】:

干得好!它绝对比@Yigit 建议的要好。竖起大拇指! 完美答案。谢谢。 (y) 难以置信,3.5 年后,漏洞仍然存在。但是,此解决方案效果很好。 我们什么时候需要这样做?搬家之后还是搬家之前?? @Andreas Wenger,这段代码是否应该在 onMove() 方法中实现?我无法让它工作......【参考方案2】:

移动物品后致电scrollToPosition(0)。不幸的是,我假设,LinearLayoutManager 试图保持第一个项目稳定,它移动所以它移动列表。

【讨论】:

虽然这解决了我的问题,但看看这个code.google.com/p/android/issues/detail?id=99047 感谢您的报告。我们会修复它。带来不便敬请谅解。幸运的是,有一个相对简单的解决方法。顺便说一句,scrollToPosition 只是将视图带到可见视口,因此即使您没有移动第一项,也可以一直调用它。 谢谢你!我使用 ItemTouchHelper 来拖动项目,并且在将第一个项目拖动到第二个项目时遇到了问题。因为滚动。花了 3 天时间弄清楚出了什么问题!!! 我遇到了同样的问题。我尝试在 onMove() 方法和 onChildDraw() 方法中调用 layoutManager 上的 scrollToPosition(0),但无法使其正常工作。我应该在哪里实现此代码?【参考方案3】:

翻译@Andreas Wenger 对 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)

在我的情况下,视图可以有一个上边距,这也需要计入偏移量,否则recyclerview不会滚动到预期的位置。为此,只需编写:

val topMargin = (firstView.layoutParams as? MarginLayoutParams)?.topMargin ?: 0
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - topMargin

如果你的项目中有 ktx 依赖,那就更容易了:

offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - firstView.marginTop

【讨论】:

【参考方案4】:

我也遇到过同样的问题。建议没有任何帮助。每个解决方案都修复和打破了不同的情况。 但是这种解决方法对我有用:

    adapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() 
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) 
            if (fromPosition == 0 || toPosition == 0)
                binding.recycler.scrollToPosition(0)
        
    )

它有助于防止在移动第一个项目时滚动案例:直接 notifyItemMoved 和通过 ItemTouchHelper(拖放)

【讨论】:

【参考方案5】:

我也遇到过同样的问题。就我而言,滚动发生在第一个可见项目上(不仅在数据集中的第一个项目上)。我要感谢大家,因为他们的回答帮助我解决了这个问题。 我基于Andreas Wenger' answer 和resoluti0n' answer 启发了我的解决方案

而且,这是我的解决方案(在 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)
    


然后,您可以将其用作(例如片段):

RecyclerViewOnDragFistItemScrollSuppressor.observe(
            viewLifecycleOwner,
            binding.recyclerView
        )

【讨论】:

【参考方案6】:

LinearLayoutManager 在LinearLayoutManager.prepareForDrop 中为您完成了这项工作。

您只需要提供移动(旧)视图和目标(新)视图。

layoutManager.prepareForDrop(oldView, targetView, -1, -1)
// the numbers, x and y don't matter to LinearLayoutManager's implementation of prepareForDrop

这是一个“非官方”API,因为它在源代码中声明

// 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) 
    ...

但它仍然有效并且完全按照其他答案所说的那样进行,为您完成所有考虑布局方向的偏移计算。

这实际上与 LinearLayoutManager 在被 ItemTouchHelper 用来解决这个可怕的错误时调用的方法相同。

【讨论】:

嗨@Bassam Helal,你在哪里调用这个方法?我无法让它工作。我尝试了 onMove 方法,其中“oldView”和“targetView”作为参数传递......

以上是关于RecyclerView.Adapter.notifyItemMoved(0,1) 滚动屏幕的主要内容,如果未能解决你的问题,请参考以下文章