Как перетащить элемент в RecyclerView работать с библиотекой подкачки вместе?
Мое приложение имеет RecyclerView, который поддерживает перетаскивание элементов, чтобы изменить их порядок. Мое приложение использует ViewModel, Lifecycle, Room до добавления библиотеки подкачки. И код для обработки перетаскивания легко.
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val oPosition = viewHolder.adapterPosition
val tPosition = target.adapterPosition
Collections.swap(adapter?.data ,oPosition,tPosition)
adapter?.notifyItemMoved(oPosition,tPosition)
//save to db
return true
}
Однако после того, как я использую библиотеку подкачки,
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val oPosition = viewHolder.adapterPosition
val tPosition = target.adapterPosition
Collections.swap(adapter.currentList,oPosition,tPosition)
adapter.notifyItemMoved(oPosition,tPosition)
return true
}
мое приложение упало, потому что PagedListAdapter.currentList не поддерживает набор.
java.lang.UnsupportedOperationException
at java.util.AbstractList.set(AbstractList.java:132)
at java.util.Collections.swap(Collections.java:539)
at gmail.zebulon988.tasklist.ui.TaskListFragment$MyItemTouchCallback.onMove(TaskListFragment.kt:119).
Затем я меняю код
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val oPosition = viewHolder.adapterPosition
val tPosition = target.adapterPosition
Log.d("TAG","onMove:o=$oPosition,t=$tPosition")
val oTask = (viewHolder as VH).task
val tTask = (target as VH).task
if(oTask != null && tTask != null){
val tmp = oTask.order
oTask.order = tTask.order
tTask.order = tmp
tasklistViewModel.insertTask(oTask,tTask)
}
return true
}
Этот код изменяет порядок задачи непосредственно в БД, а библиотека обновляет порядок отображения при изменении БД. Однако анимация ужасная.
Есть ли способ использовать onMove
а также paging library
вместе genteelly?
3 ответа
Когда вы используете PagedList с Room, вы часто связываете его так, что обновления базовых данных отражаются автоматически через LiveData или Rx, и такое обновление, происходящее в фоновом режиме, всегда может испортить ваше перетаскивание. Так что ИМХО, вы не можете сделать его на 100% пуленепробиваемым для всех ситуаций. Сказав это, вы можете создать (я почти сказал "взломать вместе") прокладку, которая будет делать то, что вы хотите. Это включает в себя несколько частей:
- Вам нужно хранить индексы предметов, которые меняются местами в вашем адаптере
- Вам нужно переопределить getItem() в адаптере и заставить его "поменять" элементы вместо вас, а не поменять их местами с помощью Collections.swap
Вам нужно отложить обновление самого предмета через Комнату до тех пор, пока предмет не будет отброшен, после чего вы также очищаете свое состояние "Идет обмен". Что-то в этом роде:
fun swapItems(fromPosition: Int, toPosition: Int) { swapInfo = SwapInfo(fromPosition, toPosition) notifyItemMoved(fromPosition, toPosition) } override fun getItem(position: Int): T? { return swapInfo?.let { when (position) { it.fromPosition -> super.getItem(it.toPosition) it.toPosition -> super.getItem(it.fromPosition) else -> super.getItem(position) } } ?: super.getItem(position) } fun clearSwapInfo() { swapInfo = null }
Таким образом, вы получите плавное перетаскивание, если в вашем списке нет фоновых обновлений и вы останетесь в уже загруженном списке элементов. Это становится намного сложнее, если вам нужно перетаскивать "пополнение".
Вам нужно проверить перемещение элементов в PagedList.
Адаптер Recyclerview должен идеально выполнять две вещи, если вы хотите перетаскивать элементы вверх и вниз для их перемещения. Первый - поменять местами два элемента в списке данных, второй - уведомить о повторной визуализации ячеек.
повторный рендеринг прост, вы можете использовать notifyItemMoved
для обновления макета при перемещении, но PagedList неизменен, вы не можете его изменить.
И есть ошибка анимации, когда пользовательский интерфейс ячейки уже изменился, а источник данных - нет. Вы не можете переопределить логику рендеринга внутри recyclerview, но вы можете проверить результатPagedStorageDiffHelper.computeDiff
чтобы исправить ошибку анимации.
Наконец, не забудьте получить самые свежие данные после перетаскивания.
//ItemTouchHelperAdapter
override fun onItemStartMove() {
//the most the most updated data; mimic pagedlist, but can be modified;
tempList = adapter.currentList?.toMutableList()
toUpdate = mutableListOf()
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
val itemFrom = tempList?.get(fromPosition) ?: return false
val itemTo = tempList?.get(toPosition) ?: return false
//change order property for data itself
val order = itemTo.order
itemTo.order = itemFrom.order
itemFrom.order = order
//save them for later update db in batch
toUpdate?.removeAll { it.id == itemFrom.id || it.id == itemTo.id }
toUpdate?.add(itemFrom)
toUpdate?.add(itemTo)
//mimic mutable pagedlist, for get next time get correct items for continuing drag
Collections.swap(tempList!!, fromPosition, toPosition)
//update ui
adapter.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemEndMove() {
tempList = null
if (!toUpdate.isNullOrEmpty()) {
mViewModel.viewModelScope.launch(Dispatchers.IO) {
//heck, fix animation bug because pagedList did not really change.
NoteListAdapter.disableAnimation = true
mViewModel.updateInDB(toUpdate!!)
toUpdate = null
}
}
}
//Fragment
mViewModel.data.observe(this.viewLifecycleOwner, Observer {
adapter.submitList(it)
//delay for fix PagedStorageDiffHelper.computeDiff running in background thread
if (NoteListAdapter.disableAnimation) {
mViewModel.viewModelScope.launch {
delay(500)
adapter.notifyDataSetChanged() //update viewholder's binding data
NoteListAdapter.disableAnimation = false
}
}
})
//PagedListAdapter
companion object {
//heck for drag and drop to move items in PagedList
var disableAnimation = false
private val DiffCallback = object : DiffUtil.ItemCallback<Note>() {
override fun areItemsTheSame(old: Note, aNew: Note): Boolean {
return disableAnimation || old.id == aNew.id
}
override fun areContentsTheSame(old: Note, aNew: Note): Boolean {
return disableAnimation || old == aNew
}
}
}
У меня была немного другая проблема, и user3195954 , наконец, привел меня к решению после нескольких часов отладки.
Для меня проблема заключалась в том, что элемент, который я только что переместил, внезапно прыгнул обратно в свою предыдущую позицию после обновления базы данных и вызова. По сути, действие перетаскивания как бы отменено, однако порядок со всеми соответствующими данными в базе данных правильный, и если бы я должен был позвонить
notifyDataSetChanged()
Я бы увидел настоящий список, где все элементы находятся там, где они должны быть. Вот что сработало для меня:
class SomePagingAdapter(
private val onItemMoveUpdate: (fromPos: Int, toPos: Int) -> Unit,
) : PagingDataAdapter<Model, SomePagingAdapter.ViewHolder>(diffUtil), ItemMoveCallback {
companion object {
private val diffUtil = /* ... */
}
private var swapInfo: SwapInfo? = null
// viewHolder methods, etc.
// Called in touch helper's onMove
override fun onItemMove(fromPos: Int, toPos: Int) {
notifyItemMoved(fromPos, toPos)
}
// Called in touch helper's clearView() to save the result of this drag and drop
override fun onItemFinishedMove(fromPos: Int, toPos: Int) {
swapInfo = SwapInfo(fromPos, toPos)
onItemMoveUpdate(fromPos, toPos)
}
fun adjustRecentSwapPositions() {
// "Undo" the notifyItemMoved we did before that messed up positions
swapInfo?.let { swap ->
notifyItemMoved(swap.toPos, swap.fromPos)
}
swapInfo = null
}
}
interface ItemMoveCallback {
fun onItemMove(fromPos: Int, toPos: Int)
fun onItemFinishedMove(fromPos: Int, toPos: Int)
}
data class SwapInfo(val fromPos: Int, val toPos: int)
Важно, что
submitData
приостановлено и
adjustRecentSwapPositions
вызывается сразу после. Следите за этим, если вы используете RxJava.
scope.launch {
flow.collectLatest { pagingData ->
adapter.submitData(pagingData)
adapter.adjustRecentSwapPositions()
}
}
Он отлично работает, и анимация ресайклера в порядке.