【问题标题】:Add Drag and Drop on RecyclerView with DiffUtil使用 DiffUtil 在 RecyclerView 上添加拖放
【发布时间】:2021-09-27 17:19:52
【问题描述】:

我有一个从房间数据库更新的列表。我从 Room 接收更新的数据作为新列表,然后将其传递给 ListAdaptersubmitList 以获取更改的动画。

list.observe(viewLifecycleOwner, { updatedList ->
    listAdapter.submitList(updatedList)
})

现在,我想为同一个 RecyclerView 添加拖放功能。我尝试使用ItemTouchHelper 来实现它。但是,notifyItemMoved() 不起作用,因为 ListAdapter 通过 submitList() 更新其内容。

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {

    val from = viewHolder.bindingAdapterPosition
    val to = target.bindingAdapterPosition

    val list = itemListAdapter.currentList.toMutableList()
    Collections.swap(list, from, to)

    // does not work for ListAdapter
    // itemListAdapter.notifyItemMoved(from, to)

    itemListAdapter.submitList(list)

    return false
}

现在拖放工作正常,但只有在缓慢拖动时,当拖动速度足够快时,我会得到不同且不一致的结果。

这可能是什么原因?为使用 ListAdapter 的 RecyclerView 实现拖放功能的最佳方式是什么?

【问题讨论】:

  • 已经有一段时间了,但位置不应该是 adapterPosition 而不是 bindingAdapterPosition 还是因为 ViewBinding 而出现的新事物?
  • adapterPosition 现在已弃用并替换为 absoluteAdapterPositionbindingAdapterPosition
  • 啊,谢谢。我应该更频繁地阅读文档。我认为问题在于将集合复制(到可变列表),然后移动项目,最后提交列表需要使用 Diff Util 来计算更改所需的“时间”。其中大部分是异步的,因此可能是时间问题。您在列表中使用 DiffUtil 或 AsyncDiffUtil 吗?你能展示你的diff Util吗?你有多少物品?您在 onItemMoved 中看到了多少次调用(当您拖动时)?
  • 是的,这可能是时间问题。 ListAdapter 使用 AsyncListDiffer。我的 diff util 比较 id 的 onareItemsTheSame,然后比较 areContentsTheSame 的内容。我什至尝试在没有包含许多内容的自定义对象的情况下做到这一点,而只使用字符串但仍然遇到同样的问题。 onItemMoved 被称为与缓慢拖动时位置变化的数量相同的数量,并且在快速拖动时不一致的计数(小于假设的数量,因为有些被跳过)。
  • (不相关)但您可以使用 Kotlin 的价值捕获魔法保存 Collections.Swap 调用:list[from] = list[to].also { list[to] = list[from] }(尽管我认为这对于新手和基本上任何阅读此代码的人来说都比较模糊) :)

标签: android android-recyclerview listadapter


【解决方案1】:

所以我做了一个快速测试(这整件事不适合评论,所以我正在写一个答案)

我的 Activity 包含适配器 RV,并观察一个 viewModel。当 ViewModel 通过 LiveData 从 repo 推送初始列表时,我以可变形式保存列表的本地副本(仅出于此测试的目的),以便我可以快速对其进行快速变异。

这是我的“onMove”实现:

val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition

list[from] = list[to].also { list[to] = list[from] }

adapter.submitList(list)
return true

我还添加了这个日志来验证一些东西:

Log.d("###", "onMove from: $from (${list[from].id}) to: $to (${list[to].id})")

我注意到了.. 有效。但是因为我返回true(你似乎返回false)。

现在...不幸的是,如果您快速上下拖动,这会导致列表最终变成随机播放:

例如:假设从 0 到 9 有 10 个项目。

您想抓住项目 0 并将其放在项目 1 和 2 之间。

您从位置 0 开始拖动项目 0,然后将其稍微向下移动,现在它位于 1 和 2 之间,onMove 方法中的新项目位置为 1(到目前为止,您仍在拖动)。现在,如果您再慢慢拖动(到位置 2),onMove 方法是从 1 到 2,而不是从 0 到 2。这是因为我返回“true”,所以每个 onMove 都是“完成操作”。这很好,因为操作很慢,而且 ListAdapter 有时间更新和计算内容。

但是当你快速拖动时,在适配器有时间赶上之前,操作就会不同步。

如果你返回false(就像你一样),那么你会得到各种其他效果:

  • RecyclerView 动画不会播放(当您拖动时),因为 viewHolders 尚未“移动”。 (你返回 false)
  • onMove 方法会在您每次将手指移到 viewHolder 上时发送垃圾邮件,因为框架希望再次执行此移动...但列表已被修改...

所以你会得到类似的东西(上面类似的例子,10 个项目,移动项目 0)> 假设每个项目都有一个对应于其位置+1 的 ID(在初始状态,所以位置 0 的项目有id 1,位置 1 的项目的 id 为 2,依此类推)

当我将项目 0 缓慢“向下”拖动时,日志显示如下: (格式为`from: position(item from) to: position(id of item to)

onMove from: 0 (1) to: 1 (2) // Initial drag of first item down to 2nd item.
onMove from: 0 (2) to: 1 (1) // now the list is inverted, notice the IDs.
onMove from: 0 (1) to: 1 (2) // Back to square one.
onMove from: 0 (2) to: 1 (1) // and undo-again...

我只是把它剪在那里,但你可以看到它是如何在整个地方来回弹跳的。我相信这是因为您返回 false 但在幕后修改列表,所以它变得混乱。在等式的一侧,“数据”说明了一件事,(diff util 也是如此),但另一方面,适配器没有注意到这种变化,至少在计算完成之前“还没有”,其中,你猜到了,当你拖得超快时,时间不够用。

很遗憾,我(今天)没有一个很好的答案来说明最好的方法是什么。也许,不依赖 ListAdapter 的行为并实现一个普通的适配器,您可以更好地控制数据的列表/源代码,何时 调用 submitList 以及何时简单地 notifyItemChanged 或在两个位置之间移动可能是这个用例的更好选择。

为无用的答案道歉。

【讨论】:

  • 感谢您的详细解释。我尝试实现一个新的适配器,而不是依赖于 ListAdapter 的行为on this answer,并且到目前为止它按预期工作。谢谢。
【解决方案2】:

Martin Marconcini's answer 所述,我最终实现了一个新适配器并使用它而不是 ListAdapter。我添加了两个单独的功能。一个用于从 Room 数据库接收更新(从 ListAdapter 替换 submitList),另一个用于从拖动中的每个位置更改

MyListAdapter.kt

class MyListAdapter(list: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // save instance instead of creating a new one every submit 
    // list to save some allocation time. Thanks to Martin Marconcini
    private val diffCallback = DiffCallback(list, ArrayList())

    fun submitList(updatedList: List<Item>) {
        diffCallback.newList = updatedList
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        list.clear()
        list.addAll(updatedList)
        diffResult.dispatchUpdatesTo(this)
    }

    fun itemMoved(from: Int, to: Int) {
        Collections.swap(list, from, to)
        notifyItemMoved(from, to)
    }

}

DiffCallback.kt

class DiffCallback(
    val oldList: List<Item>,
    var newList: List<Item>
) : DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return compareContents(oldItem, newItem)
    }
}

每次职位变动都致电itemMoved

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {
    val from = viewHolder.bindingAdapterPosition
    val to = target.bindingAdapterPosition
    itemListAdapter.itemMoved(from, to)

    // Update database as well if needed

    return true
}

从 Room 数据库接收更新时:

您可能还想检查当前是否使用onSelectedChanged 进行拖动,如果您还在每次位置更改时更新您的数据库,以防止对submitList 的不必要调用

list.observe(viewLifecycleOwner, { updatedList ->
    listAdapter.submitList(updatedList)
})

【讨论】:

  • 很好的解决方案;一个微小的优化是保存 DiffUtilCallback 实例,而不是在提交列表时创建新实例,因为这些可以重复使用。它将为您节省一些分配时间。您可以随时拨打DiffUtil.calculateDiff(callback)
猜你喜欢
  • 2023-03-13
  • 2015-07-06
  • 2019-06-06
  • 2017-06-23
  • 2022-07-07
  • 2020-06-04
  • 2018-03-17
  • 2017-09-13
  • 1970-01-01
相关资源
最近更新 更多