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

xeufq47z  于 2022-11-03  发布在  Android
关注(0)|答案(2)|浏览(221)

目前,我有一个实现新ListAdapter的RecyclerView,它使用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的例子来实现拖放和他们的(简单)列表和布局工作得很好。我的列表是有点复杂,因为它是在一个视图页,使用导航组件和有许多其他视图约束在一起,在该屏幕上,但我不认为这应该是相关的。
在这一点上,我甚至不知道如何在网络上搜索这个问题了。我找到的最接近的解决方案可能是https://issuetracker.google.com/issues/37018279,但在实现并具有相同的行为后,我认为这是因为我使用了不同的ListAdapter,并异步更新列表,而解决方案使用了RecyclerView.Adapter,它使用notifyItemMoved和其他类似的方法。
正在切换到RecyclerView。适配器不是解决方案。

8yoxcaq7

8yoxcaq71#

这似乎是AsyncListDiffer中的一个bug,它被ListAdapter暗中使用。我的解决方案允许你在需要的时候手动比较更改。然而,它相当笨拙,使用反射,可能不能在未来的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问题跟踪器链接更新问题。

omtl5h9j

omtl5h9j2#

我在我的适配器中保留了一个项目的副本,修改了副本,并使用notifyItemMoved在用户拖动时更新UI。我只在用户完成拖动后保存更新的项目/顺序。这对我很有效,因为1)我有一个固定长度的9个项目的列表; 2)我能够使用clearView来知道拖动何时结束。

列表适配器-Kotlin:

var myItems: MutableList<MyItem> = mutableListOf()

fun onMove(fromPosition: Int, toPosition: Int): Boolean {
    if (fromPosition < toPosition) {
        for (i in fromPosition until toPosition) {
            Collections.swap(myItems, i, i + 1)
        }
    } else {
        for (i in fromPosition downTo toPosition + 1) {
            Collections.swap(myItems, i, i - 1)
        }
    }
    notifyItemMoved(fromPosition, toPosition)
    return true
}

项目触摸助手.回调()-Kotlin:

// my items are only ever selected during drag, so when selection clears, drag has ended
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
    super.clearView(recyclerView, viewHolder)
    // clear drag style after item moved
    viewHolder.itemView.requestLayout()
    // trigger callback after item moved
    val itemViewHolder = viewHolder as MyItemViewHolder
    itemViewHolder.onItemMovedCallback(adapter.myItems)
}

我的项目视图持有者-Kotlin

fun onItemMovedCallback(reorderedItems: List<MyItem>) {
    // user has finished drag
    // save new item order to database or submit list properly to adapter
}

我在MyItem上也有一个itemOrder字段。当我将它保存到DB时,我使用重新排序的条目的索引更新了该字段。当我交换条目时,我可能会更新每个条目的itemOrder字段,但这似乎毫无意义(我只是在拖动完成后计算新的顺序)。
我正在使用数据库中的LiveData。我发现在最后一次数据库保存后视图“闪烁”,因为我更改了所有项目上的itemOrder,并在适配器列表中移动了项目。如果您遇到这种情况,并且不喜欢它,只是暂时禁用回收程序视图项目动画(我通过在拖动后将其设置为null,并在RecyclerView/Adapter中更新列表后恢复它来实现这一点)。
这对我和我的具体情况都有效。如果你需要更多细节,请告诉我。

相关问题