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
}
// 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) {
// Translate the adapter indices into the correct queue indices
val delta = mQueue.value!!.size - formattedQueue.value!!.size
@ -265,13 +266,11 @@ class PlaybackViewModel : ViewModel() {
try {
val currentItem = mQueue.value!![from]
val targetItem = mQueue.value!![to]
// Then swap the items manually since kotlin does have a swap function.
mQueue.value!![to] = currentItem
mQueue.value!![from] = targetItem
mQueue.value!!.removeAt(from)
mQueue.value!!.add(to, currentItem)
} 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
}
@ -279,7 +278,8 @@ class PlaybackViewModel : ViewModel() {
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) {
// Translate the adapter index into the correct queue index
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.viewholders.BaseViewHolder
// FIXME: Build a Diff function so that QueueAdapter doesn't scroll wildly when things are moved
class QueueAdapter(private val dragCallback: ItemTouchHelper) :
ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) {
class QueueAdapter(
val touchHelper: ItemTouchHelper
) : ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)))
}
@ -23,7 +22,7 @@ class QueueAdapter(private val dragCallback: ItemTouchHelper) :
holder.bind(getItem(position))
}
// Generic ViewHolder for a detail album
// Generic ViewHolder for a queue item
inner class ViewHolder(
private val binding: ItemQueueSongBinding,
) : BaseViewHolder<Song>(binding, null) {
@ -35,7 +34,7 @@ class QueueAdapter(private val dragCallback: ItemTouchHelper) :
binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
dragCallback.startDrag(this)
touchHelper.startDrag(this)
return@setOnTouchListener true
}

View file

@ -4,14 +4,17 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.playback.PlaybackViewModel
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
class QueueDragCallback(private val playbackModel: PlaybackViewModel) :
ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START
) {
// The drag callback used for the Queue RecyclerView.
class QueueDragCallback(
private val playbackModel: PlaybackViewModel
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START
) {
override fun interpolateOutOfBoundsScroll(
recyclerView: RecyclerView,
viewSize: Int,
@ -19,11 +22,14 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) :
totalSize: Int,
msSinceStartScroll: Long
): 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(
recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll
)
val clampedAbsVelocity = Math.max(
val clampedAbsVelocity = max(
MINIMUM_INITIAL_DRAG_VELOCITY,
min(
abs(standardSpeed),

View file

@ -27,8 +27,9 @@ class QueueFragment : BottomSheetDialogFragment() {
): View? {
val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
val helper = ItemTouchHelper(
QueueDragCallback(playbackModel)
)
val queueAdapter = QueueAdapter(helper)
// --- UI SETUP ---
@ -36,9 +37,9 @@ class QueueFragment : BottomSheetDialogFragment() {
binding.queueHeader.setTextColor(accent.first.toColor(requireContext()))
binding.queueRecycler.apply {
adapter = queueAdapter
itemAnimator = DefaultItemAnimator()
applyDivider()
setHasFixedSize(true)
itemAnimator = DefaultItemAnimator()
helper.attachToRecyclerView(this)
}
@ -46,7 +47,15 @@ class QueueFragment : BottomSheetDialogFragment() {
// --- VIEWMODEL SETUP ---
playbackModel.formattedQueue.observe(viewLifecycleOwner) {
queueAdapter.submitList(it.toMutableList())
// 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())
}
}
return binding.root

View file

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

View file

@ -66,11 +66,12 @@
<ImageView
android:id="@+id/song_drag_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_handle"
android:layout_height="0dp"
android:padding="@dimen/padding_small"
android:clickable="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_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />