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

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

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

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

遗憾的是,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);
}

翻译@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

LinearLayoutManager 已在 LinearLayoutManager.prepareForDrop.

中为您完成此操作

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

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

它是一个 "unofficial" 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 用来解决这个可怕的错误时调用的方法相同。

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

    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(拖放)

我遇到了同样的问题。在我的例子中,滚动发生在第一个可见项目上(不仅在数据集中的第一个项目上)。我要感谢大家,因为他们的回答帮助我解决了这个问题。 我基于 and from

启发了我的解决方案

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