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
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<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;
} else if (lastNestedScrollDy == 0) {
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
// otherwise settle to closest expanded state.
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
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<V extends View> 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<V extends View> 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.
*

View file

@ -68,6 +68,9 @@ class PlaybackSheetBehavior<V : View>(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

View file

@ -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<QueueViewModel.QueueSong, QueueItemListener, QueueSongViewHolder>(listener) {
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(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<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 {
@ -46,7 +79,7 @@ interface QueueItemListener {
class QueueSongViewHolder
private constructor(
private val binding: ItemQueueSongBinding,
) : BindingViewHolder<QueueViewModel.QueueSong, QueueItemListener>(binding.root) {
) : BindingViewHolder<Song, QueueItemListener>(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<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
}
val DIFFER = SongViewHolder.DIFFER
}
}

View file

@ -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<FragmentQueueBinding>(), QueueItemList
// --- VIEWMODEL SETUP ----
collectImmediately(queueModel.queue, ::updateQueue)
collectImmediately(queueModel.queue, queueModel.index, ::updateQueue)
}
override fun onDestroyBinding(binding: FragmentQueueBinding) {
@ -82,16 +83,20 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
touchHelper.startDrag(viewHolder)
}
private fun updateQueue(queue: List<QueueViewModel.QueueSong>) {
val instructions = queueModel.instructions
if (instructions != null) {
if (instructions.replace) {
private fun updateQueue(queue: List<Song>, index: Int) {
val replaceQueue = queueModel.replaceQueue
if (replaceQueue == true) {
logD("Replacing queue")
queueAdapter.data.replaceList(queue)
} else {
logD("Diffing queue")
queueAdapter.data.submitList(queue)
}
if (instructions.scrollTo != null) {
queueModel.finishReplace()
val scrollTo = queueModel.scrollTo
if (scrollTo != null) {
val binding = requireBinding()
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
val start = lmm.findFirstCompletelyVisibleItemPosition()
@ -99,14 +104,13 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
if (start != RecyclerView.NO_POSITION &&
end != RecyclerView.NO_POSITION &&
instructions.scrollTo !in start..end) {
binding.queueRecycler.scrollToPosition(instructions.scrollTo)
scrollTo !in start..end) {
binding.queueRecycler.scrollToPosition(scrollTo)
}
}
queueModel.finishInstructions()
} else {
queueAdapter.data.submitList(queue)
}
queueModel.finishScrollTo()
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)
init {
isHideable = false
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.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<Song>())
val queue: StateFlow<List<Song>> = _queue
private val _queue = MutableStateFlow(listOf<QueueSong>())
val queue: StateFlow<List<QueueSong>> = _queue
private val _index = MutableStateFlow(playbackManager.index)
val index: StateFlow<Int>
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<Song>) {
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<Song>) {
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<Song>, parent: MusicParent?) {
instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex))
_queue.value = generateQueue(index, queue)
}
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
replaceQueue = true
scrollTo = min(index + 1, playbackManager.queue.lastIndex)
_queue.value = playbackManager.queue.toMutableList()
_index.value = index
}
override fun onCleared() {

View file

@ -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">
<TextView
android:id="@+id/playback_song"

View file

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