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

由于mDifferListAdapter 中是私有的,您需要直接使用它,因此您必须创建自己的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 的第一项会移动几个随机位置的主要内容,如果未能解决你的问题,请参考以下文章

如何在顶部的第一个项目装饰后剪辑 recyclerView

当列表填满屏幕时,RecyclerView 显示第一项的副本

如何拖放 recyclerView 元素?

未添加要插入移动数据库的第一项

RecyclerView 拖放喜欢 - 文件和文件夹

如何添加静态项目,例如“添加新联系人”,始终作为回收站视图的第一项? [关闭]