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:
parent
b42dfd0b53
commit
f5542c65ba
8 changed files with 129 additions and 87 deletions
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,31 +83,34 @@ 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) {
|
||||
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<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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue