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.
This commit is contained in:
OxygenCobalt 2022-07-31 16:50:47 -06:00
parent b42dfd0b53
commit f5542c65ba
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 129 additions and 87 deletions

View file

@ -720,7 +720,8 @@ public class NeoBottomSheetBehavior<V extends View> extends CoordinatorLayout.Be
} }
} else if (dy < 0) { // Downward } else if (dy < 0) { // Downward
if (!target.canScrollVertically(-1)) { if (!target.canScrollVertically(-1)) {
if (newTop <= collapsedOffset || hideable) { // MODIFICATION: Add enableHidingGestures method
if (newTop <= collapsedOffset || (hideable && enableHidingGestures())) {
if (!draggable) { if (!draggable) {
// Prevent dragging // Prevent dragging
return; return;
@ -774,7 +775,8 @@ public class NeoBottomSheetBehavior<V extends View> extends CoordinatorLayout.Be
} }
} }
} }
} else if (hideable && shouldHide(child, getYVelocity())) { // MODIFICATION: Add enableHidingGestures method
} else if (hideable && shouldHide(child, getYVelocity()) && enableHidingGestures()) {
targetState = STATE_HIDDEN; targetState = STATE_HIDDEN;
} else if (lastNestedScrollDy == 0) { } else if (lastNestedScrollDy == 0) {
int currentTop = child.getTop(); int currentTop = child.getTop();
@ -1723,7 +1725,8 @@ public class NeoBottomSheetBehavior<V extends View> 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 // Hide if the view was either released low or it was a significant vertical swipe
// otherwise settle to closest expanded state. // otherwise settle to closest expanded state.
if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD) if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD)
@ -1795,8 +1798,9 @@ public class NeoBottomSheetBehavior<V extends View> extends CoordinatorLayout.Be
@Override @Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) { public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// MODIFICATION: Add enableHidingGestures method
return MathUtils.clamp( return MathUtils.clamp(
top, getExpandedOffset(), hideable ? parentHeight : collapsedOffset); top, getExpandedOffset(), (hideable && enableHidingGestures()) ? parentHeight : collapsedOffset);
} }
@Override @Override
@ -1806,7 +1810,8 @@ public class NeoBottomSheetBehavior<V extends View> extends CoordinatorLayout.Be
@Override @Override
public int getViewVerticalDragRange(@NonNull View child) { public int getViewVerticalDragRange(@NonNull View child) {
if (hideable) { // MODIFICATION: Add enableHidingGestures method
if (hideable && enableHidingGestures()) {
return parentHeight; return parentHeight;
} else { } else {
return collapsedOffset; return collapsedOffset;
@ -1876,6 +1881,15 @@ public class NeoBottomSheetBehavior<V extends View> extends CoordinatorLayout.Be
return true; 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. * Checks whether the bottom sheet should be expanded after it has been released after dragging.
* *

View file

@ -68,6 +68,9 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
return success return success
} }
// Note: This is an extension to Auxio's vendored BottomSheetBehavior
override fun enableHidingGestures() = true
fun hideSafe() { fun hideSafe() {
if (state != STATE_HIDDEN) { if (state != STATE_HIDDEN) {
isDraggable = false isDraggable = false

View file

@ -29,13 +29,46 @@ import java.util.*
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.ui.recycler.*
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
class QueueAdapter(listener: QueueItemListener) : class QueueAdapter(listener: QueueItemListener) :
MonoAdapter<QueueViewModel.QueueSong, QueueItemListener, QueueSongViewHolder>(listener) { MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
private var currentIndex = 0
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER)
override val creator = QueueSongViewHolder.CREATOR override val creator = QueueSongViewHolder.CREATOR
override fun onBindViewHolder(
viewHolder: QueueSongViewHolder,
position: Int,
payload: List<Any>
) {
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 { interface QueueItemListener {
@ -46,7 +79,7 @@ interface QueueItemListener {
class QueueSongViewHolder class QueueSongViewHolder
private constructor( private constructor(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BindingViewHolder<QueueViewModel.QueueSong, QueueItemListener>(binding.root) { ) : BindingViewHolder<Song, QueueItemListener>(binding.root) {
val bodyView: View val bodyView: View
get() = binding.body get() = binding.body
val backgroundView: View val backgroundView: View
@ -58,8 +91,15 @@ private constructor(
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5 elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5
} }
val isPrevious: Boolean var isPrevious: Boolean
get() = binding.songDragHandle.alpha == 0.5f 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 { init {
binding.body.background = binding.body.background =
@ -75,10 +115,10 @@ private constructor(
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun bind(item: QueueViewModel.QueueSong, listener: QueueItemListener) { override fun bind(item: Song, listener: QueueItemListener) {
binding.songAlbumCover.bind(item.song) binding.songAlbumCover.bind(item)
binding.songName.textSafe = item.song.resolveName(binding.context) binding.songName.textSafe = item.resolveName(binding.context)
binding.songInfo.textSafe = item.song.resolveIndividualArtistName(binding.context) binding.songInfo.textSafe = item.resolveIndividualArtistName(binding.context)
binding.background.isInvisible = true binding.background.isInvisible = true
@ -87,12 +127,6 @@ private constructor(
binding.body.setOnClickListener { listener.onClick(this) } 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 // Roll our own drag handlers as the default ones suck
binding.songDragHandle.setOnTouchListener { _, motionEvent -> binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick() binding.songDragHandle.performClick()
@ -113,19 +147,6 @@ private constructor(
QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater))
} }
val DIFFER = val DIFFER = SongViewHolder.DIFFER
object : SimpleItemCallback<QueueViewModel.QueueSong>() {
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
}
} }
} }

View file

@ -25,10 +25,11 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.*
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/** /**
* A [Fragment] that shows the queue and enables editing as well. * A [Fragment] that shows the queue and enables editing as well.
@ -66,7 +67,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
collectImmediately(queueModel.queue, ::updateQueue) collectImmediately(queueModel.queue, queueModel.index, ::updateQueue)
} }
override fun onDestroyBinding(binding: FragmentQueueBinding) { override fun onDestroyBinding(binding: FragmentQueueBinding) {
@ -82,16 +83,20 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
touchHelper.startDrag(viewHolder) touchHelper.startDrag(viewHolder)
} }
private fun updateQueue(queue: List<QueueViewModel.QueueSong>) { private fun updateQueue(queue: List<Song>, index: Int) {
val instructions = queueModel.instructions val replaceQueue = queueModel.replaceQueue
if (instructions != null) { if (replaceQueue == true) {
if (instructions.replace) { logD("Replacing queue")
queueAdapter.data.replaceList(queue) queueAdapter.data.replaceList(queue)
} else { } else {
logD("Diffing queue")
queueAdapter.data.submitList(queue) queueAdapter.data.submitList(queue)
} }
if (instructions.scrollTo != null) { queueModel.finishReplace()
val scrollTo = queueModel.scrollTo
if (scrollTo != null) {
val binding = requireBinding() val binding = requireBinding()
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
val start = lmm.findFirstCompletelyVisibleItemPosition() val start = lmm.findFirstCompletelyVisibleItemPosition()
@ -99,14 +104,13 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
if (start != RecyclerView.NO_POSITION && if (start != RecyclerView.NO_POSITION &&
end != RecyclerView.NO_POSITION && end != RecyclerView.NO_POSITION &&
instructions.scrollTo !in start..end) { scrollTo !in start..end) {
binding.queueRecycler.scrollToPosition(instructions.scrollTo) binding.queueRecycler.scrollToPosition(scrollTo)
} }
} }
queueModel.finishInstructions() queueModel.finishScrollTo()
} else {
queueAdapter.data.submitList(queue) queueAdapter.updateIndex(index)
}
} }
} }

View file

@ -33,6 +33,7 @@ class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?
private var barSpacing = context.getDimenSizeSafe(R.dimen.spacing_small) private var barSpacing = context.getDimenSizeSafe(R.dimen.spacing_small)
init { init {
isHideable = false
sheetBackgroundDrawable.setCornerSize(context.getDimenSafe(R.dimen.size_corners_medium)) sheetBackgroundDrawable.setCornerSize(context.getDimenSafe(R.dimen.size_corners_medium))
} }

View file

@ -24,21 +24,19 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.ui.recycler.Item
class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
data class QueueSong(val song: Song, val previous: Boolean) : Item() { private val _queue = MutableStateFlow(listOf<Song>())
override val id: Long val queue: StateFlow<List<Song>> = _queue
get() = song.id
}
private val _queue = MutableStateFlow(listOf<QueueSong>()) private val _index = MutableStateFlow(playbackManager.index)
val queue: StateFlow<List<QueueSong>> = _queue val index: StateFlow<Int>
get() = _index
data class QueueInstructions(val replace: Boolean, val scrollTo: Int?) var replaceQueue: Boolean? = null
var instructions: QueueInstructions? = null var scrollTo: Int? = null
init { init {
playbackManager.addCallback(this) playbackManager.addCallback(this)
@ -76,35 +74,38 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
return true return true
} }
fun finishInstructions() { fun finishReplace() {
instructions = null replaceQueue = null
}
fun finishScrollTo() {
scrollTo = null
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
instructions = QueueInstructions(false, min(index + 1, playbackManager.queue.lastIndex)) replaceQueue = null
_queue.value = generateQueue(index, playbackManager.queue) scrollTo = min(index + 1, playbackManager.queue.lastIndex)
_index.value = index
} }
override fun onQueueChanged(queue: List<Song>) { override fun onQueueChanged(queue: List<Song>) {
instructions = QueueInstructions(false, null) replaceQueue = false
_queue.value = generateQueue(playbackManager.index, queue) scrollTo = null
_queue.value = playbackManager.queue.toMutableList()
} }
override fun onQueueReworked(index: Int, queue: List<Song>) { override fun onQueueReworked(index: Int, queue: List<Song>) {
instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) replaceQueue = true
_queue.value = generateQueue(index, queue) scrollTo = min(index + 1, playbackManager.queue.lastIndex)
_queue.value = playbackManager.queue.toMutableList()
_index.value = index
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) replaceQueue = true
_queue.value = generateQueue(index, queue) scrollTo = min(index + 1, playbackManager.queue.lastIndex)
} _queue.value = playbackManager.queue.toMutableList()
_index.value = index
private fun generateQueue(index: Int, queue: List<Song>): List<QueueSong> {
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
} }
override fun onCleared() { override fun onCleared() {

View file

@ -38,8 +38,7 @@
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/playback_cover" app:layout_constraintStart_toEndOf="@+id/playback_cover"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed">
app:trackColorInactive="@color/sel_track">
<TextView <TextView
android:id="@+id/playback_song" android:id="@+id/playback_song"

View file

@ -87,8 +87,7 @@
android:indeterminate="true" android:indeterminate="true"
app:indeterminateAnimationType="disjoint" app:indeterminateAnimationType="disjoint"
app:layout_constraintBottom_toBottomOf="@+id/home_indexing_action" app:layout_constraintBottom_toBottomOf="@+id/home_indexing_action"
app:layout_constraintTop_toTopOf="@+id/home_indexing_action" app:layout_constraintTop_toTopOf="@+id/home_indexing_action" />
app:trackColor="@color/sel_track" />
<Button <Button
android:id="@+id/home_indexing_action" android:id="@+id/home_indexing_action"