使用 DiffUtil 在 RecyclerView 上添加拖放
Add Drag and Drop on RecyclerView with DiffUtil
我有一个从房间数据库更新的列表。我从 Room 收到更新后的数据作为新列表,然后将其传递给 ListAdapter 的 submitList
以获得更改的动画。
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 实现拖放功能的最佳方法是什么?
所以我做了一个快速测试(整件事不适合评论,所以我正在写一个答案)
我的 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})")
我注意到它..有效。但是因为我returning true
(你似乎returning false)。
现在...不幸的是,如果你快速上下拖动,这会导致列表最终变得混乱:
例如:假设从 0-9 有 10 项。
您想抓取第 0 项并将其放在第 1 项和第 2 项之间。
您从位置 0 开始拖动项目 0,并将其向下移动一点,现在它在 1 和 2 之间,onMove
方法中的新项目位置为 1(到目前为止,您仍然拖动)。现在,如果你慢慢地进一步拖动(到位置 2),onMove 方法是从 1 到 2,而不是从 0 到 2。这是因为我 returned “true” 所以每个 onMove 都是一个“完成的操作”。这很好,因为操作很慢并且 ListAdapter 有时间更新和计算内容。
但是当你快速拖动时,操作在适配器有时间赶上之前就失去了同步。
如果你 return false
而不是(像你一样)那么你会得到各种其他效果:
- RecyclerView 动画不会播放(在您拖动时),因为 viewHolder 尚未“移动”。 (你 return 编错了)
- 每次您将手指移到 viewHolder 上时,
onMove
方法就会被发送垃圾邮件,因为框架想要再次执行此移动...但是列表已经被修改...
所以你会得到类似的东西(类似上面的例子,10 个项目,移动项目 0)> 假设每个项目都有一个对应于它的位置的 ID + 1(在初始状态下,所以项目在位置0 的 ID 为 1,位置 1 的项目的 ID 为 2,等等)
这是我慢慢“向下”拖动项目 0 时日志显示的内容:
(格式为`from: position(id of 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...
我只是把它剪到那里,但你可以看到它是如何在整个地方来回弹跳的。我相信这是因为你return false 但是在幕后修改了列表,所以它变得混乱了。在等式的一边,“数据”说的是一件事(diff util 也是如此),但另一方面,适配器没有注意到这种变化,至少“还”没有注意到计算完成,这就像你猜对了,当你拖得超快的时候,时间不够了。
不幸的是,对于最佳方法是什么,我(今天)没有一个好的答案。也许,不依赖于 ListAdapter 的行为并实现普通适配器,您可以更好地 list/source 控制数据和 何时 调用 submitList 以及何时简单地通知 ItemChanged 或在两者之间移动对于这个用例,两个位置可能是更好的选择。
对无用的答案表示歉意。
我最终实现了一个新的适配器,并使用它代替了 ListAdapter,如 中所述。我添加了两个独立的功能。一个用于接收来自 Room 数据库的更新(替换 submitList
来自 ListAdapter),另一个用于从 drag
中更改每个位置
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)
})
我有一个从房间数据库更新的列表。我从 Room 收到更新后的数据作为新列表,然后将其传递给 ListAdapter 的 submitList
以获得更改的动画。
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 实现拖放功能的最佳方法是什么?
所以我做了一个快速测试(整件事不适合评论,所以我正在写一个答案)
我的 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})")
我注意到它..有效。但是因为我returning true
(你似乎returning false)。
现在...不幸的是,如果你快速上下拖动,这会导致列表最终变得混乱:
例如:假设从 0-9 有 10 项。
您想抓取第 0 项并将其放在第 1 项和第 2 项之间。
您从位置 0 开始拖动项目 0,并将其向下移动一点,现在它在 1 和 2 之间,onMove
方法中的新项目位置为 1(到目前为止,您仍然拖动)。现在,如果你慢慢地进一步拖动(到位置 2),onMove 方法是从 1 到 2,而不是从 0 到 2。这是因为我 returned “true” 所以每个 onMove 都是一个“完成的操作”。这很好,因为操作很慢并且 ListAdapter 有时间更新和计算内容。
但是当你快速拖动时,操作在适配器有时间赶上之前就失去了同步。
如果你 return false
而不是(像你一样)那么你会得到各种其他效果:
- RecyclerView 动画不会播放(在您拖动时),因为 viewHolder 尚未“移动”。 (你 return 编错了)
- 每次您将手指移到 viewHolder 上时,
onMove
方法就会被发送垃圾邮件,因为框架想要再次执行此移动...但是列表已经被修改...
所以你会得到类似的东西(类似上面的例子,10 个项目,移动项目 0)> 假设每个项目都有一个对应于它的位置的 ID + 1(在初始状态下,所以项目在位置0 的 ID 为 1,位置 1 的项目的 ID 为 2,等等)
这是我慢慢“向下”拖动项目 0 时日志显示的内容: (格式为`from: position(id of 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...
我只是把它剪到那里,但你可以看到它是如何在整个地方来回弹跳的。我相信这是因为你return false 但是在幕后修改了列表,所以它变得混乱了。在等式的一边,“数据”说的是一件事(diff util 也是如此),但另一方面,适配器没有注意到这种变化,至少“还”没有注意到计算完成,这就像你猜对了,当你拖得超快的时候,时间不够了。
不幸的是,对于最佳方法是什么,我(今天)没有一个好的答案。也许,不依赖于 ListAdapter 的行为并实现普通适配器,您可以更好地 list/source 控制数据和 何时 调用 submitList 以及何时简单地通知 ItemChanged 或在两者之间移动对于这个用例,两个位置可能是更好的选择。
对无用的答案表示歉意。
我最终实现了一个新的适配器,并使用它代替了 ListAdapter,如 submitList
来自 ListAdapter),另一个用于从 drag
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)
})