Improve queue editing

Fix issue where moving queue items would swap them, not move them, also prevent QueueAdapter from scrolling uncontrollably if the first item is moved.
This commit is contained in:
OxygenCobalt 2020-10-23 19:15:10 -06:00
parent 912d296e92
commit 55ed55c5dc
6 changed files with 42 additions and 26 deletions

View file

@ -255,7 +255,8 @@ class PlaybackViewModel : ViewModel() {
mCanAnimate = false mCanAnimate = false
} }
// Move two queue items. Called by QueueDragCallback. // Move two queue items. Note that this function does not force-update the queue,
// as calling updateData with a drag would cause bugs.
fun moveQueueItems(adapterFrom: Int, adapterTo: Int) { fun moveQueueItems(adapterFrom: Int, adapterTo: Int) {
// Translate the adapter indices into the correct queue indices // Translate the adapter indices into the correct queue indices
val delta = mQueue.value!!.size - formattedQueue.value!!.size val delta = mQueue.value!!.size - formattedQueue.value!!.size
@ -265,13 +266,11 @@ class PlaybackViewModel : ViewModel() {
try { try {
val currentItem = mQueue.value!![from] val currentItem = mQueue.value!![from]
val targetItem = mQueue.value!![to]
// Then swap the items manually since kotlin does have a swap function. mQueue.value!!.removeAt(from)
mQueue.value!![to] = currentItem mQueue.value!!.add(to, currentItem)
mQueue.value!![from] = targetItem
} catch (exception: IndexOutOfBoundsException) { } catch (exception: IndexOutOfBoundsException) {
Log.e(this::class.simpleName, "Indices were out of bounds, did not swap queue items") Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item")
return return
} }
@ -279,7 +278,8 @@ class PlaybackViewModel : ViewModel() {
forceQueueUpdate() forceQueueUpdate()
} }
// Remove a queue item. Called by QueueDragCallback. // Remove a queue item. Note that this function does not force-update the queue,
// as calling updateData with a drag would cause bugs.
fun removeQueueItem(adapterIndex: Int) { fun removeQueueItem(adapterIndex: Int) {
// Translate the adapter index into the correct queue index // Translate the adapter index into the correct queue index
val delta = mQueue.value!!.size - formattedQueue.value!!.size val delta = mQueue.value!!.size - formattedQueue.value!!.size

View file

@ -11,10 +11,9 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
// FIXME: Build a Diff function so that QueueAdapter doesn't scroll wildly when things are moved class QueueAdapter(
class QueueAdapter(private val dragCallback: ItemTouchHelper) : val touchHelper: ItemTouchHelper
ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) { ) : ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))) return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)))
} }
@ -23,7 +22,7 @@ class QueueAdapter(private val dragCallback: ItemTouchHelper) :
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
// Generic ViewHolder for a detail album // Generic ViewHolder for a queue item
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BaseViewHolder<Song>(binding, null) { ) : BaseViewHolder<Song>(binding, null) {
@ -35,7 +34,7 @@ class QueueAdapter(private val dragCallback: ItemTouchHelper) :
binding.songDragHandle.performClick() binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
dragCallback.startDrag(this) touchHelper.startDrag(this)
return@setOnTouchListener true return@setOnTouchListener true
} }

View file

@ -4,14 +4,17 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.sign import kotlin.math.sign
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : // The drag callback used for the Queue RecyclerView.
ItemTouchHelper.SimpleCallback( class QueueDragCallback(
private val playbackModel: PlaybackViewModel
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START ItemTouchHelper.START
) { ) {
override fun interpolateOutOfBoundsScroll( override fun interpolateOutOfBoundsScroll(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewSize: Int, viewSize: Int,
@ -19,11 +22,14 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) :
totalSize: Int, totalSize: Int,
msSinceStartScroll: Long msSinceStartScroll: Long
): Int { ): Int {
// Fix to make QueueFragment scroll when an item is scrolled out of bounds.
// Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe
val standardSpeed = super.interpolateOutOfBoundsScroll( val standardSpeed = super.interpolateOutOfBoundsScroll(
recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll
) )
val clampedAbsVelocity = Math.max( val clampedAbsVelocity = max(
MINIMUM_INITIAL_DRAG_VELOCITY, MINIMUM_INITIAL_DRAG_VELOCITY,
min( min(
abs(standardSpeed), abs(standardSpeed),

View file

@ -27,8 +27,9 @@ class QueueFragment : BottomSheetDialogFragment() {
): View? { ): View? {
val binding = FragmentQueueBinding.inflate(inflater) val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel)) val helper = ItemTouchHelper(
QueueDragCallback(playbackModel)
)
val queueAdapter = QueueAdapter(helper) val queueAdapter = QueueAdapter(helper)
// --- UI SETUP --- // --- UI SETUP ---
@ -36,9 +37,9 @@ class QueueFragment : BottomSheetDialogFragment() {
binding.queueHeader.setTextColor(accent.first.toColor(requireContext())) binding.queueHeader.setTextColor(accent.first.toColor(requireContext()))
binding.queueRecycler.apply { binding.queueRecycler.apply {
adapter = queueAdapter adapter = queueAdapter
itemAnimator = DefaultItemAnimator()
applyDivider() applyDivider()
setHasFixedSize(true) setHasFixedSize(true)
itemAnimator = DefaultItemAnimator()
helper.attachToRecyclerView(this) helper.attachToRecyclerView(this)
} }
@ -46,8 +47,16 @@ class QueueFragment : BottomSheetDialogFragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
playbackModel.formattedQueue.observe(viewLifecycleOwner) { playbackModel.formattedQueue.observe(viewLifecycleOwner) {
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) {
queueAdapter.submitList(it.toMutableList()) {
binding.queueRecycler.scrollToPosition(0)
}
} else {
queueAdapter.submitList(it.toMutableList()) queueAdapter.submitList(it.toMutableList())
} }
}
return binding.root return binding.root
} }

View file

@ -10,6 +10,7 @@ abstract class BaseViewHolder<T : BaseModel>(
private val doOnClick: ((T) -> Unit)? private val doOnClick: ((T) -> Unit)?
) : RecyclerView.ViewHolder(baseBinding.root) { ) : RecyclerView.ViewHolder(baseBinding.root) {
init { init {
// Force the layout to *actually* be the screen width
baseBinding.root.layoutParams = RecyclerView.LayoutParams( baseBinding.root.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
) )

View file

@ -66,11 +66,12 @@
<ImageView <ImageView
android:id="@+id/song_drag_handle" android:id="@+id/song_drag_handle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:src="@drawable/ic_handle" android:padding="@dimen/padding_small"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent" android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/song_info"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />