From f5542c65ba00a15f9310d77899dea4ca948c9300 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 31 Jul 2022 16:50:47 -0600 Subject: [PATCH] queue: rework previous items Rework previous items to be based off of adapter state. This improves the transitions between active and previous items and their overall efficiency. --- .../bottomsheet/NeoBottomSheetBehavior.java | 24 ++++-- .../auxio/playback/PlaybackSheetBehavior.kt | 3 + .../auxio/playback/queue/QueueAdapter.kt | 75 ++++++++++++------- .../auxio/playback/queue/QueueFragment.kt | 54 ++++++------- .../playback/queue/QueueSheetBehavior.kt | 1 + .../auxio/playback/queue/QueueViewModel.kt | 53 ++++++------- .../fragment_playback_panel.xml | 3 +- app/src/main/res/layout/fragment_home.xml | 3 +- 8 files changed, 129 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/google/android/material/bottomsheet/NeoBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/NeoBottomSheetBehavior.java index 66f223379..79f3f5c27 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/NeoBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/NeoBottomSheetBehavior.java @@ -720,7 +720,8 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be } } else if (dy < 0) { // Downward if (!target.canScrollVertically(-1)) { - if (newTop <= collapsedOffset || hideable) { + // MODIFICATION: Add enableHidingGestures method + if (newTop <= collapsedOffset || (hideable && enableHidingGestures())) { if (!draggable) { // Prevent dragging return; @@ -774,7 +775,8 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be } } } - } else if (hideable && shouldHide(child, getYVelocity())) { + // MODIFICATION: Add enableHidingGestures method + } else if (hideable && shouldHide(child, getYVelocity()) && enableHidingGestures()) { targetState = STATE_HIDDEN; } else if (lastNestedScrollDy == 0) { int currentTop = child.getTop(); @@ -1723,7 +1725,8 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be } } } - } else if (hideable && shouldHide(releasedChild, yvel)) { + // MODIFICATION: Add enableHidingGestures method + } else if (hideable && shouldHide(releasedChild, yvel) && enableHidingGestures()) { // Hide if the view was either released low or it was a significant vertical swipe // otherwise settle to closest expanded state. if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD) @@ -1795,8 +1798,9 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { + // MODIFICATION: Add enableHidingGestures method return MathUtils.clamp( - top, getExpandedOffset(), hideable ? parentHeight : collapsedOffset); + top, getExpandedOffset(), (hideable && enableHidingGestures()) ? parentHeight : collapsedOffset); } @Override @@ -1806,7 +1810,8 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be @Override public int getViewVerticalDragRange(@NonNull View child) { - if (hideable) { + // MODIFICATION: Add enableHidingGestures method + if (hideable && enableHidingGestures()) { return parentHeight; } else { return collapsedOffset; @@ -1876,6 +1881,15 @@ public class NeoBottomSheetBehavior extends CoordinatorLayout.Be return true; } + /** + * Checks whether hiding gestures should be enabled if {@code isHideable} is true. + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public boolean enableHidingGestures() { + return true; + } + /** * Checks whether the bottom sheet should be expanded after it has been released after dragging. * diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt index c481dc208..9a1a204bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt @@ -68,6 +68,9 @@ class PlaybackSheetBehavior(context: Context, attributeSet: AttributeS return success } + // Note: This is an extension to Auxio's vendored BottomSheetBehavior + override fun enableHidingGestures() = true + fun hideSafe() { if (state != STATE_HIDDEN) { isDraggable = false diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 2ac0e2007..df57685f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -29,13 +29,46 @@ import java.util.* import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemQueueSongBinding +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.util.* class QueueAdapter(listener: QueueItemListener) : - MonoAdapter(listener) { + MonoAdapter(listener) { + private var currentIndex = 0 + override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) override val creator = QueueSongViewHolder.CREATOR + + override fun onBindViewHolder( + viewHolder: QueueSongViewHolder, + position: Int, + payload: List + ) { + if (payload.isEmpty()) { + super.onBindViewHolder(viewHolder, position, payload) + } + + viewHolder.isPrevious = position <= currentIndex + } + + fun updateIndex(index: Int) { + when { + index < currentIndex -> { + val lastIndex = currentIndex + currentIndex = index + notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_INDEX) + } + index > currentIndex -> { + currentIndex = index + notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_INDEX) + } + } + } + + companion object { + val PAYLOAD_UPDATE_INDEX = Any() + } } interface QueueItemListener { @@ -46,7 +79,7 @@ interface QueueItemListener { class QueueSongViewHolder private constructor( private val binding: ItemQueueSongBinding, -) : BindingViewHolder(binding.root) { +) : BindingViewHolder(binding.root) { val bodyView: View get() = binding.body val backgroundView: View @@ -58,8 +91,15 @@ private constructor( elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5 } - val isPrevious: Boolean + var isPrevious: Boolean get() = binding.songDragHandle.alpha == 0.5f + set(value) { + val alpha = if (value) 0.5f else 1f + binding.songAlbumCover.alpha = alpha + binding.songName.alpha = alpha + binding.songInfo.alpha = alpha + binding.songDragHandle.alpha = alpha + } init { binding.body.background = @@ -75,10 +115,10 @@ private constructor( } @SuppressLint("ClickableViewAccessibility") - override fun bind(item: QueueViewModel.QueueSong, listener: QueueItemListener) { - binding.songAlbumCover.bind(item.song) - binding.songName.textSafe = item.song.resolveName(binding.context) - binding.songInfo.textSafe = item.song.resolveIndividualArtistName(binding.context) + override fun bind(item: Song, listener: QueueItemListener) { + binding.songAlbumCover.bind(item) + binding.songName.textSafe = item.resolveName(binding.context) + binding.songInfo.textSafe = item.resolveIndividualArtistName(binding.context) binding.background.isInvisible = true @@ -87,12 +127,6 @@ private constructor( binding.body.setOnClickListener { listener.onClick(this) } - val alpha = if (item.previous) 0.5f else 1f - binding.songAlbumCover.alpha = alpha - binding.songName.alpha = alpha - binding.songInfo.alpha = alpha - binding.songDragHandle.alpha = alpha - // Roll our own drag handlers as the default ones suck binding.songDragHandle.setOnTouchListener { _, motionEvent -> binding.songDragHandle.performClick() @@ -113,19 +147,6 @@ private constructor( QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) } - val DIFFER = - object : SimpleItemCallback() { - override fun areContentsTheSame( - oldItem: QueueViewModel.QueueSong, - newItem: QueueViewModel.QueueSong - ) = - super.areContentsTheSame(oldItem, newItem) && - oldItem.previous == newItem.previous - - override fun areItemsTheSame( - oldItem: QueueViewModel.QueueSong, - newItem: QueueViewModel.QueueSong - ) = oldItem.song == newItem.song && oldItem.previous == newItem.previous - } + val DIFFER = SongViewHolder.DIFFER } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index ce24c2587..8c8bb65e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -25,10 +25,11 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import java.util.* import org.oxycblt.auxio.databinding.FragmentQueueBinding +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD /** * A [Fragment] that shows the queue and enables editing as well. @@ -66,7 +67,7 @@ class QueueFragment : ViewBindingFragment(), QueueItemList // --- VIEWMODEL SETUP ---- - collectImmediately(queueModel.queue, ::updateQueue) + collectImmediately(queueModel.queue, queueModel.index, ::updateQueue) } override fun onDestroyBinding(binding: FragmentQueueBinding) { @@ -82,31 +83,34 @@ class QueueFragment : ViewBindingFragment(), QueueItemList touchHelper.startDrag(viewHolder) } - private fun updateQueue(queue: List) { - val instructions = queueModel.instructions - if (instructions != null) { - if (instructions.replace) { - queueAdapter.data.replaceList(queue) - } else { - queueAdapter.data.submitList(queue) - } - - if (instructions.scrollTo != null) { - val binding = requireBinding() - val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager - val start = lmm.findFirstCompletelyVisibleItemPosition() - val end = lmm.findLastCompletelyVisibleItemPosition() - - if (start != RecyclerView.NO_POSITION && - end != RecyclerView.NO_POSITION && - instructions.scrollTo !in start..end) { - binding.queueRecycler.scrollToPosition(instructions.scrollTo) - } - } - - queueModel.finishInstructions() + private fun updateQueue(queue: List, index: Int) { + val replaceQueue = queueModel.replaceQueue + if (replaceQueue == true) { + logD("Replacing queue") + queueAdapter.data.replaceList(queue) } else { + logD("Diffing queue") queueAdapter.data.submitList(queue) } + + queueModel.finishReplace() + + val scrollTo = queueModel.scrollTo + if (scrollTo != null) { + val binding = requireBinding() + val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager + val start = lmm.findFirstCompletelyVisibleItemPosition() + val end = lmm.findLastCompletelyVisibleItemPosition() + + if (start != RecyclerView.NO_POSITION && + end != RecyclerView.NO_POSITION && + scrollTo !in start..end) { + binding.queueRecycler.scrollToPosition(scrollTo) + } + } + + queueModel.finishScrollTo() + + queueAdapter.updateIndex(index) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt index 5235171d4..89d323f99 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt @@ -33,6 +33,7 @@ class QueueSheetBehavior(context: Context, attributeSet: AttributeSet? private var barSpacing = context.getDimenSizeSafe(R.dimen.spacing_small) init { + isHideable = false sheetBackgroundDrawable.setCornerSize(context.getDimenSafe(R.dimen.size_corners_medium)) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 711d551d3..c36f9f13a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -24,21 +24,19 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.ui.recycler.Item class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { private val playbackManager = PlaybackStateManager.getInstance() - data class QueueSong(val song: Song, val previous: Boolean) : Item() { - override val id: Long - get() = song.id - } + private val _queue = MutableStateFlow(listOf()) + val queue: StateFlow> = _queue - private val _queue = MutableStateFlow(listOf()) - val queue: StateFlow> = _queue + private val _index = MutableStateFlow(playbackManager.index) + val index: StateFlow + get() = _index - data class QueueInstructions(val replace: Boolean, val scrollTo: Int?) - var instructions: QueueInstructions? = null + var replaceQueue: Boolean? = null + var scrollTo: Int? = null init { playbackManager.addCallback(this) @@ -76,35 +74,38 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { return true } - fun finishInstructions() { - instructions = null + fun finishReplace() { + replaceQueue = null + } + + fun finishScrollTo() { + scrollTo = null } override fun onIndexMoved(index: Int) { - instructions = QueueInstructions(false, min(index + 1, playbackManager.queue.lastIndex)) - _queue.value = generateQueue(index, playbackManager.queue) + replaceQueue = null + scrollTo = min(index + 1, playbackManager.queue.lastIndex) + _index.value = index } override fun onQueueChanged(queue: List) { - instructions = QueueInstructions(false, null) - _queue.value = generateQueue(playbackManager.index, queue) + replaceQueue = false + scrollTo = null + _queue.value = playbackManager.queue.toMutableList() } override fun onQueueReworked(index: Int, queue: List) { - instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) - _queue.value = generateQueue(index, queue) + replaceQueue = true + scrollTo = min(index + 1, playbackManager.queue.lastIndex) + _queue.value = playbackManager.queue.toMutableList() + _index.value = index } override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { - instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) - _queue.value = generateQueue(index, queue) - } - - private fun generateQueue(index: Int, queue: List): List { - val before = queue.slice(0..index).map { QueueSong(it, true) } - val after = - queue.slice(index + 1..playbackManager.queue.lastIndex).map { QueueSong(it, false) } - return before + after + replaceQueue = true + scrollTo = min(index + 1, playbackManager.queue.lastIndex) + _queue.value = playbackManager.queue.toMutableList() + _index.value = index } override fun onCleared() { diff --git a/app/src/main/res/layout-w600dp-land/fragment_playback_panel.xml b/app/src/main/res/layout-w600dp-land/fragment_playback_panel.xml index 50776f631..3e7931eb5 100644 --- a/app/src/main/res/layout-w600dp-land/fragment_playback_panel.xml +++ b/app/src/main/res/layout-w600dp-land/fragment_playback_panel.xml @@ -38,8 +38,7 @@ app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/playback_cover" app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" - app:layout_constraintVertical_chainStyle="packed" - app:trackColorInactive="@color/sel_track"> + app:layout_constraintVertical_chainStyle="packed"> + app:layout_constraintTop_toTopOf="@+id/home_indexing_action" />