拖放 RecyclerView 的第一项会移动几个随机位置
Posted
技术标签:
【中文标题】拖放 RecyclerView 的第一项会移动几个随机位置【英文标题】:Drag & Dropping the first item of the RecyclerView moves several random positions 【发布时间】:2021-04-24 12:50:28 【问题描述】:目前,我有一个 RecyclerView 实现了新的 ListAdapter,使用 submitList 来区分不同的元素并继续自动更新 UI。
最近我不得不使用众所周知的 ItemTouchHelper 实现拖放到列表。这是我的实现,非常简单:
class DraggableItemTouchHelper(private val adapter: DestinationsAdapter) : ItemTouchHelper.Callback()
private val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
private val swipeFlags = 0
override fun isLongPressDragEnabled() = false
override fun isItemViewSwipeEnabled() = false
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int
return makeMovementFlags(dragFlags, swipeFlags)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean
val oldPos = viewHolder.adapterPosition
val newPos = target.adapterPosition
adapter.swap(oldPos, newPos)
return true
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int)
这是我在适配器内部的交换函数:
fun swap(from: Int, to: Int)
submitList(ArrayList(currentList).also
it[from] = currentList[to]
it[to] = currentList[from]
)
除了移动列表的第一项时,一切正常。有时它的行为还可以,但大多数时候(如 90%),即使将其移动到第二个项目的上方(例如将第一个项目移动到第二个位置),它也会捕捉多个位置。新职位似乎是随机的,我无法弄清楚问题所在。
作为指导,我使用https://github.com/material-components/material-components-android 示例来实现拖放,并且它们的(简单)列表和布局效果很好。我的列表有点复杂,因为它在 viewpager 中,使用 Navigation 组件,并且在该屏幕中约束在一起的许多其他视图,但我认为这不应该相关。
此时我什至不知道如何在网上搜索此问题。 我为此找到的最接近的解决方案可能是https://issuetracker.google.com/issues/37018279,但在实现并具有相同的行为之后,我认为这是因为我使用不同的 ListAdapter 并异步更新列表,当解决方案使用 RecyclerView.Adapter 它使用 notifyItemMoved 和其他类似方法。
切换到 RecyclerView.Adapter 不是解决方案。
【问题讨论】:
这里有同样的问题。对我来说,当没有项目超出可见屏幕空间时拖动第一个项目,它工作正常。但是,当屏幕上有项目时拖动它(recyclerView 是可滚动的)然后它会跳转多个位置。这是一个模式,如果屏幕外有 1 个项目,第一个项目会跳 2 个位置。如果屏幕外有 2 个项目,则它会跳转 3 个位置,依此类推。它具有这种 (n, n+1) 行为,其中 n = 屏幕外的项目数。我已经在 3 个模拟器和一个物理设备上验证了这种模式。这是你的经历吗? 所以经过更多研究,我发现第一个项目没有捕捉,而是整个屏幕都在滚动(在我的情况下,一直向下显示屏幕外的最后一个项目,无论出于何种原因) .尽管here 的答案对我没有帮助,但他们帮助了其他人,希望对您有所帮助。 @gig6 我遇到了同样的问题。我的问题的所有细节都和你的一样。我一直在尝试找到一种方法将列表提交给ListAdapter
,而不是让它通知RecyclerView
的更改,所以我可以自己通过调用notifyItemMoved()
来完成(这样它工作得很好)。但是,我没有找到实现这一目标的方法。也许我应该为此提交功能请求,或者报告 Diff Util 在所描述的情况下表现异常的错误。
@gig6 我实际上设法找到了解决方案:在下面查看我的答案。
【参考方案1】:
这似乎是AsyncListDiffer
中的一个错误,ListAdapter
在后台使用了它。我的解决方案可让您在需要时手动区分更改。但是,它相当 hacky,使用反射,并且可能不适用于未来的 appcompat
版本(我测试过的版本是 1.3.0
)。
由于mDiffer
在ListAdapter
中是私有的,您需要直接使用它,因此您必须创建自己的ListAdapter
实现(您可以复制原始源代码)。然后添加如下方法:
fun setListWithoutDiffing(list: List<T>)
setOf("mList", "mReadOnlyList").forEach fieldName ->
val field = mDiffer::class.java.getDeclaredField(fieldName)
field.isAccessible = true
field.set(mDiffer, list)
此方法静默更改底层AsyncListDiffer
中的当前列表,而不会触发任何差异,就像submitList()
那样。
生成的文件应如下所示:
package com.example.yourapp
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
abstract class ListAdapter<T, VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>
private val mDiffer: AsyncListDiffer<T>
private val mListener =
ListListener<T> previousList, currentList -> onCurrentListChanged(previousList, currentList)
protected constructor(diffCallback: DiffUtil.ItemCallback<T>)
mDiffer = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
).apply
addListListener(mListener)
protected constructor(config: AsyncDifferConfig<T>)
mDiffer = AsyncListDiffer(AdapterListUpdateCallback(this), config).apply
addListListener(mListener)
fun setListWithoutDiffing(list: List<T>)
setOf("mList", "mReadOnlyList").forEach fieldName ->
val field = mDiffer::class.java.getDeclaredField(fieldName)
field.isAccessible = true
field.set(mDiffer, list)
open fun submitList(list: List<T>?)
mDiffer.submitList(list)
fun submitList(list: List<T>?, commitCallback: Runnable?)
mDiffer.submitList(list, commitCallback)
protected fun getItem(position: Int): T
return mDiffer.currentList[position]
override fun getItemCount(): Int
return mDiffer.currentList.size
val currentList: List<T>
get() = mDiffer.currentList
open fun onCurrentListChanged(previousList: List<T>, currentList: List<T>)
现在您需要更改您的适配器实现以继承您的自定义 ListAdapter
而不是 androidx.recyclerview.widget.ListAdapter
。
最后,您需要更改适配器的 swap()
方法实现以使用 setListWithoutDiffing()
和 notifyItemMoved()
方法:
fun swap(from: Int, to: Int)
setListWithoutDiffing(ArrayList(currentList).also
it[from] = currentList[to]
it[to] = currentList[from]
)
notifyItemMoved(from, to)
另一种解决方案是创建一个自定义的AsyncListDiffer
版本,让您在没有反射的情况下做同样的事情,但这种方式似乎更容易。我还将提交功能请求以支持开箱即用的手动差异,并使用 Google 问题跟踪器链接更新问题。
【讨论】:
以上是关于拖放 RecyclerView 的第一项会移动几个随机位置的主要内容,如果未能解决你的问题,请参考以下文章