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

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

【问题讨论】:

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

标签: android android-adapter android-recyclerview


【解决方案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' answerresoluti0n' 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”作为参数传递......
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-11-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多