android - 拖放 RecyclerView 的第一项会移动几个随机位置

标签 android android-recyclerview scroll drag-and-drop

目前,我有一个 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 组件,并且在该屏幕中约束在一起的许多其他 View ,但我认为这不应该相关。
在这一点上,我什至不知道如何在网上搜索这个问题了。
我为此找到的最接近的解决方案可能是 https://issuetracker.google.com/issues/37018279但是在实现并具有相同的行为之后,我认为这是因为我使用不同的 ListAdapter 并异步更新列表,当解决方案使用 RecyclerView.Adapter 它使用 notifyItemMoved 和其他类似方法时。
切换到 RecyclerView.Adapter 不是解决方案。

最佳答案

这似乎是 AsyncListDiffer 中的一个错误, ListAdapter 在后台使用.我的解决方案可让您在需要时手动区分更改。但是,它相当 hacky,使用反射,并且可能不适用于 future appcompat版本(我测试过的版本是 1.3.0 )。
由于mDifferListAdapter 中是私有(private)的你需要直接使用它,你必须创建自己的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 问题跟踪器链接更新问题。

关于android - 拖放 RecyclerView 的第一项会移动几个随机位置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65804749/

相关文章:

android - 获取 Android BluetoothDevice 的重命名名称

Android:垂直 Recyclerview 内的水平 Recyclerview

javascript - 缓慢地自动滚动到 div 的底部

android - 无法在 Tabhost 派生 Activity 中滚动选项卡内容

jquery - 如何使用 jQuery 滚动到 Kendo 网格中的选定行?

javascript - React Native NavigationContainer 不显示任何内容

java - 使用同名节点的 Android/Java XML 解析

android - Python 的多处理管道不工作

java - 如何使用卡片向 RecyclerView 添加 searchBar

android - 带有数据绑定(bind)的约束布局没有响应