mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-12 13:32:11 +03:00
Fix ItemTouchHelper usage in the PlayerFragment
This commit is contained in:
parent
acb25c06e3
commit
bcf56375c6
@ -11,13 +11,12 @@ import android.annotation.SuppressLint
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
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.DiffUtil
|
||||||
import com.drakeet.multitype.MultiTypeAdapter
|
import com.drakeet.multitype.MultiTypeAdapter
|
||||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.util.BoundedTreeSet
|
import org.moire.ultrasonic.util.BoundedTreeSet
|
||||||
|
import org.moire.ultrasonic.util.SettableAsyncListDiffer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,23 +58,33 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
|||||||
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
private var mDiffer: SettableAsyncListDiffer<T> = SettableAsyncListDiffer(
|
||||||
AdapterListUpdateCallback(this),
|
AdapterListUpdateCallback(this),
|
||||||
AsyncDifferConfig.Builder(diffCallback).build()
|
AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val mListener =
|
private val mListener: SettableAsyncListDiffer.ListListener<T> =
|
||||||
ListListener<T> { previousList, currentList ->
|
object : SettableAsyncListDiffer.ListListener<T> {
|
||||||
this@BaseAdapter.onCurrentListChanged(
|
override fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {
|
||||||
previousList,
|
this@BaseAdapter.onCurrentListChanged(
|
||||||
currentList
|
previousList,
|
||||||
)
|
currentList
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mDiffer.addListListener(mListener)
|
mDiffer.addListListener(mListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the List to a new value without invoking the diffing
|
||||||
|
*/
|
||||||
|
fun setList(newList: List<T>) {
|
||||||
|
Timber.v("SetList updated list in differ, size %s", newList.size)
|
||||||
|
mDiffer.setList(newList)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits a new list to be diffed, and displayed.
|
* Submits a new list to be diffed, and displayed.
|
||||||
*
|
*
|
||||||
|
@ -869,18 +869,21 @@ class PlayerFragment :
|
|||||||
val from = viewHolder.bindingAdapterPosition
|
val from = viewHolder.bindingAdapterPosition
|
||||||
val to = target.bindingAdapterPosition
|
val to = target.bindingAdapterPosition
|
||||||
|
|
||||||
Timber.i("MOVING from %d to %d", from, to)
|
// The item must be moved manually in the viewAdapter, because it must be
|
||||||
val newList = viewAdapter.getCurrentList().toMutableList()
|
// moved synchronously, before this function returns. AsyncListDiffer would execute
|
||||||
|
// the move too late
|
||||||
|
val items = viewAdapter.getCurrentList().toMutableList()
|
||||||
if (from < to) {
|
if (from < to) {
|
||||||
for (i in from until to) {
|
for (i in from until to) {
|
||||||
Collections.swap(newList, i, i + 1)
|
Collections.swap(items, i, i + 1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (i in from downTo to + 1) {
|
for (i in from downTo to + 1) {
|
||||||
Collections.swap(newList, i, i - 1)
|
Collections.swap(items, i, i - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewAdapter.submitList(newList)
|
viewAdapter.setList(items)
|
||||||
|
viewAdapter.notifyItemMoved(from, to)
|
||||||
endPosition = to
|
endPosition = to
|
||||||
|
|
||||||
// When the user moves an item, onMove may be called many times quickly,
|
// When the user moves an item, onMove may be called many times quickly,
|
||||||
@ -896,7 +899,12 @@ class PlayerFragment :
|
|||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
val pos = viewHolder.bindingAdapterPosition
|
val pos = viewHolder.bindingAdapterPosition
|
||||||
val item = mediaPlayerController.getMediaItemAt(pos)
|
val item = mediaPlayerController.getMediaItemAt(pos)
|
||||||
mediaPlayerController.removeFromPlaylist(pos)
|
|
||||||
|
// Remove the item from the list quickly
|
||||||
|
val items = viewAdapter.getCurrentList().toMutableList()
|
||||||
|
items.removeAt(pos)
|
||||||
|
viewAdapter.setList(items)
|
||||||
|
viewAdapter.notifyItemRemoved(pos)
|
||||||
|
|
||||||
val songRemoved = String.format(
|
val songRemoved = String.format(
|
||||||
resources.getString(R.string.download_song_removed),
|
resources.getString(R.string.download_song_removed),
|
||||||
@ -904,6 +912,9 @@ class PlayerFragment :
|
|||||||
)
|
)
|
||||||
|
|
||||||
Util.toast(context, songRemoved)
|
Util.toast(context, songRemoved)
|
||||||
|
|
||||||
|
// Remove the item from the playlist
|
||||||
|
mediaPlayerController.removeFromPlaylist(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(
|
||||||
|
@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
* SettableAsyncListDiffer.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a variation of the AsyncListDiffer provided for the RecyclerView.
|
||||||
|
* It is possible to set its List to a new value without triggering the diffing.
|
||||||
|
* This is necessary when the changes in the List must be synchronous, and are
|
||||||
|
* executed manually elsewhere in the code.
|
||||||
|
* @see androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
* More discussion about the necessity of this class is in the merge request:
|
||||||
|
* https://gitlab.com/ultrasonic/ultrasonic/-/merge_requests/815
|
||||||
|
* We opened an issue for Google and hope they will add an official solution, see:
|
||||||
|
* https://issuetracker.google.com/issues/247351552
|
||||||
|
*/
|
||||||
|
class SettableAsyncListDiffer<T> {
|
||||||
|
private var mUpdateCallback: ListUpdateCallback? = null
|
||||||
|
var mConfig: AsyncDifferConfig<T>? = null
|
||||||
|
private var mMainThreadExecutor: Executor? = null
|
||||||
|
|
||||||
|
private class MainThreadExecutor : Executor {
|
||||||
|
val mHandler = Handler(Looper.getMainLooper())
|
||||||
|
override fun execute(command: Runnable) {
|
||||||
|
mHandler.post(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use MainThreadExecutor from supportlib once one exists
|
||||||
|
private val sMainThreadExecutor: Executor = MainThreadExecutor()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for when the current List is updated.
|
||||||
|
*
|
||||||
|
* @param <T> Type of items in List
|
||||||
|
</T> */
|
||||||
|
interface ListListener<T> {
|
||||||
|
/**
|
||||||
|
* Called after the current List has been updated.
|
||||||
|
*
|
||||||
|
* @param previousList The previous list.
|
||||||
|
* @param currentList The new current list.
|
||||||
|
*/
|
||||||
|
fun onCurrentListChanged(previousList: List<T>, currentList: List<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mListeners: MutableList<ListListener<T>> = CopyOnWriteArrayList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience for
|
||||||
|
* `AsyncListDiffer(new AdapterListUpdateCallback(adapter),
|
||||||
|
* new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());`
|
||||||
|
*
|
||||||
|
* @param adapter Adapter to dispatch position updates to.
|
||||||
|
* @param diffCallback ItemCallback that compares items to dispatch appropriate animations when
|
||||||
|
*
|
||||||
|
* @see DiffUtil.DiffResult.dispatchUpdatesTo
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
adapter: RecyclerView.Adapter<*>,
|
||||||
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
|
) : this(
|
||||||
|
AdapterListUpdateCallback(adapter),
|
||||||
|
AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch
|
||||||
|
* updates to.
|
||||||
|
*
|
||||||
|
* @param listUpdateCallback Callback to dispatch updates to.
|
||||||
|
* @param config Config to define background work Executor, and DiffUtil.ItemCallback for
|
||||||
|
* computing List diffs.
|
||||||
|
*
|
||||||
|
* @see DiffUtil.DiffResult.dispatchUpdatesTo
|
||||||
|
*/
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
constructor(
|
||||||
|
listUpdateCallback: ListUpdateCallback,
|
||||||
|
config: AsyncDifferConfig<T>
|
||||||
|
) {
|
||||||
|
mUpdateCallback = listUpdateCallback
|
||||||
|
mConfig = config
|
||||||
|
mMainThreadExecutor = if (config.mainThreadExecutor != null) {
|
||||||
|
config.mainThreadExecutor
|
||||||
|
} else {
|
||||||
|
sMainThreadExecutor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mList: List<T>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-null, unmodifiable version of mList.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise
|
||||||
|
*/
|
||||||
|
private var mReadOnlyList = emptyList<T>()
|
||||||
|
|
||||||
|
// Max generation of currently scheduled runnable
|
||||||
|
private var mMaxScheduledGeneration/* synthetic access */ = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current List - any diffing to present this list has already been computed and
|
||||||
|
* dispatched via the ListUpdateCallback.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If a `null` List, or no List has been submitted, an empty list will be returned.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The returned list may not be mutated - mutations to content must be done through
|
||||||
|
* [.submitList].
|
||||||
|
*
|
||||||
|
* @return current List.
|
||||||
|
*/
|
||||||
|
|
||||||
|
val currentList: List<T>
|
||||||
|
get() = mReadOnlyList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the List to a new value without invoking the diffing
|
||||||
|
*/
|
||||||
|
fun setList(newList: List<T>) {
|
||||||
|
mList = newList
|
||||||
|
mReadOnlyList = Collections.unmodifiableList(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
|
||||||
|
* thread.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If a List is already present, a diff will be computed asynchronously on a background thread.
|
||||||
|
* When the diff is computed, it will be applied (dispatched to the [ListUpdateCallback]),
|
||||||
|
* and the new List will be swapped in.
|
||||||
|
*
|
||||||
|
* @param newList The new List.
|
||||||
|
*/
|
||||||
|
fun submitList(newList: List<T>?) {
|
||||||
|
submitList(newList, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
|
||||||
|
* thread.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If a List is already present, a diff will be computed asynchronously on a background thread.
|
||||||
|
* When the diff is computed, it will be applied (dispatched to the [ListUpdateCallback]),
|
||||||
|
* and the new List will be swapped in.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The commit callback can be used to know when the List is committed, but note that it
|
||||||
|
* may not be executed. If List B is submitted immediately after List A, and is
|
||||||
|
* committed directly, the callback associated with List A will not be run.
|
||||||
|
*
|
||||||
|
* @param newList The new List.
|
||||||
|
* @param commitCallback Optional runnable that is executed when the List is committed, if
|
||||||
|
* it is committed.
|
||||||
|
*/
|
||||||
|
fun submitList(
|
||||||
|
newList: List<T>?,
|
||||||
|
commitCallback: Runnable?
|
||||||
|
) {
|
||||||
|
// incrementing generation means any currently-running diffs are discarded when they finish
|
||||||
|
val runGeneration = ++mMaxScheduledGeneration
|
||||||
|
if (newList === mList) {
|
||||||
|
// nothing to do (Note - still had to inc generation, since may have ongoing work)
|
||||||
|
commitCallback?.run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val previousList = mReadOnlyList
|
||||||
|
|
||||||
|
// fast simple remove all
|
||||||
|
if (newList == null) {
|
||||||
|
val countRemoved = mList!!.size
|
||||||
|
mList = null
|
||||||
|
mReadOnlyList = emptyList()
|
||||||
|
// notify last, after list is updated
|
||||||
|
mUpdateCallback!!.onRemoved(0, countRemoved)
|
||||||
|
onCurrentListChanged(previousList, commitCallback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast simple first insert
|
||||||
|
if (mList == null) {
|
||||||
|
mList = newList
|
||||||
|
mReadOnlyList = Collections.unmodifiableList(newList)
|
||||||
|
// notify last, after list is updated
|
||||||
|
mUpdateCallback!!.onInserted(0, newList.size)
|
||||||
|
onCurrentListChanged(previousList, commitCallback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val oldList: List<T> = mList!!
|
||||||
|
mConfig!!.backgroundThreadExecutor.execute {
|
||||||
|
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int {
|
||||||
|
return oldList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNewListSize(): Int {
|
||||||
|
return newList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
return if (oldItem != null && newItem != null) {
|
||||||
|
mConfig!!.diffCallback.areItemsTheSame(oldItem, newItem)
|
||||||
|
} else oldItem == null && newItem == null
|
||||||
|
// If both items are null we consider them the same.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
if (oldItem != null && newItem != null) {
|
||||||
|
return mConfig!!.diffCallback.areContentsTheSame(oldItem, newItem)
|
||||||
|
}
|
||||||
|
if (oldItem == null && newItem == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
if (oldItem != null && newItem != null) {
|
||||||
|
return mConfig!!.diffCallback.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mMainThreadExecutor!!.execute {
|
||||||
|
if (mMaxScheduledGeneration == runGeneration) {
|
||||||
|
latchList(newList, result, commitCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun latchList(
|
||||||
|
newList: List<T>,
|
||||||
|
diffResult: DiffResult,
|
||||||
|
commitCallback: Runnable?
|
||||||
|
) {
|
||||||
|
val previousList = mReadOnlyList
|
||||||
|
mList = newList
|
||||||
|
// notify last, after list is updated
|
||||||
|
mReadOnlyList = Collections.unmodifiableList(newList)
|
||||||
|
diffResult.dispatchUpdatesTo(mUpdateCallback!!)
|
||||||
|
onCurrentListChanged(previousList, commitCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCurrentListChanged(
|
||||||
|
previousList: List<T>,
|
||||||
|
commitCallback: Runnable?
|
||||||
|
) {
|
||||||
|
// current list is always mReadOnlyList
|
||||||
|
for (listener in mListeners) {
|
||||||
|
listener.onCurrentListChanged(previousList, mReadOnlyList)
|
||||||
|
}
|
||||||
|
commitCallback?.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a ListListener to receive updates when the current List changes.
|
||||||
|
*
|
||||||
|
* @param listener Listener to receive updates.
|
||||||
|
*
|
||||||
|
* @see .getCurrentList
|
||||||
|
* @see .removeListListener
|
||||||
|
*/
|
||||||
|
fun addListListener(listener: ListListener<T>) {
|
||||||
|
mListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a previously registered ListListener.
|
||||||
|
*
|
||||||
|
* @param listener Previously registered listener.
|
||||||
|
* @see .getCurrentList
|
||||||
|
* @see .addListListener
|
||||||
|
*/
|
||||||
|
fun removeListListener(listener: ListListener<T>) {
|
||||||
|
mListeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user