Как перетащить элемент в 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% пуленепробиваемым для всех ситуаций. Сказав это, вы можете создать (я почти сказал "взломать вместе") прокладку, которая будет делать то, что вы хотите. Это включает в себя несколько частей:

  1. Вам нужно хранить индексы предметов, которые меняются местами в вашем адаптере
  2. Вам нужно переопределить getItem() в адаптере и заставить его "поменять" элементы вместо вас, а не поменять их местами с помощью Collections.swap
  3. Вам нужно отложить обновление самого предмета через Комнату до тех пор, пока предмет не будет отброшен, после чего вы также очищаете свое состояние "Идет обмен". Что-то в этом роде:

    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()
    }
}

Он отлично работает, и анимация ресайклера в порядке.

Другие вопросы по тегам