拖放 RecyclerView 的第一项会移动多个随机位置

Gab*_*abi 6 android scroll drag-and-drop android-recyclerview

目前,我有一个 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) {
}
Run Code Online (Sandbox Code Playgroud)

}

这是我在适配器内的交换函数:

fun swap(from: Int, to: Int) {
    submitList(ArrayList(currentList).also {
        it[from] = currentList[to]
        it[to] = currentList[from]
    })
}
Run Code Online (Sandbox Code Playgroud)

除了移动列表的第一个项目外,一切都运行良好。有时它表现得很好,但大多数时候(比如 90%),即使将其稍微移动到第二个项目上方(例如将第一个项目移动到第二个位置),它也会捕捉多个位置。新位置似乎是随机的,我无法弄清楚问题所在。

作为指导,我使用https://github.com/material-components/material-components-android示例来实现拖放,并且其(简单的)列表和布局效果很好。我的列表有点复杂,因为它位于视图分页器内,使用导航组件并且在该屏幕中将许多其他视图限制在一起,但我认为这不应该相关。

此时我什至不知道如何在网络上搜索这个问题了。我为此找到的最接近的解决方案可能是https://issuetracker.google.com/issues/37018279但在实现并具有相同的行为之后,我认为这是因为我使用 ListAdapter ,它不同并异步更新列表,当解决方案使用RecyclerView.Adapter,它使用notifyItemMoved和其他类似的方法。

切换到 RecyclerView.Adapter 并不是一个解决方案。

Max*_*Max 4

这似乎是 中的一个错误AsyncListDiffer,由ListAdapter. 我的解决方案允许您在需要时手动比较更改。然而,它相当老套,使用反射,并且可能不适用于未来的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)
    }
}
Run Code Online (Sandbox Code Playgroud)

此方法会默默地更改底层中的当前列表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>) {}
}

Run Code Online (Sandbox Code Playgroud)

现在您需要更改适配器实现以继承您的自定义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)
}
Run Code Online (Sandbox Code Playgroud)

另一种解决方案是创建一个自定义AsyncListDiffer版本,让您无需反射即可执行相同操作,但这种方式似乎更简单。我还将提交一个功能请求,以支持开箱即用的手动比较,并使用 Google 问题跟踪器链接更新问题。