From c3d85090692cbc0e7d44c5f15069691c63805f36 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sat, 30 Jul 2022 16:06:17 -0600 Subject: [PATCH] ui: optimize bottom sheets Desperately try to minimize the amount of layouts my bottom sheet code is producing. It still relayouts twice in one pass. I hate android. --- CHANGELOG.md | 1 + app/build.gradle | 4 +- .../java/org/oxycblt/auxio/MainFragment.kt | 13 ++-- .../auxio/playback/PlaybackBarFragment.kt | 5 +- .../auxio/playback/queue/QueueDragCallback.kt | 25 ------ .../auxio/playback/queue/QueueFragment.kt | 4 +- .../auxio/playback/queue/QueueViewModel.kt | 7 +- .../auxio/ui/BottomSheetContentBehavior.kt | 77 ++++++++++++------- .../auxio/ui/recycler/EdgeRecyclerView.kt | 6 +- 9 files changed, 74 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b9f003c..16b669572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ at the cost of longer loading times - Queue can now be swiped up [#92] - Playing song is now shown in queue [#92] - Added ability to play songs from queue [#92] + - Added ability to see previous songs in queue - Added Last Added sorting - Search now takes sort tags and file names in account [#184] - Added option to clear playback state in settings diff --git a/app/build.gradle b/app/build.gradle index a9ebf6401..d4962fbb7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { defaultConfig { applicationId namespace - versionName "2.5.0" - versionCode 18 + versionName "2.6.0-beta" + versionCode 19 // API 33 is still busted, waiting until the XML element issue is fixed // noinspection OldTargetApi diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index e9936bc0f..d22bcbe11 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -131,19 +131,19 @@ class MainFragment : val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f) val queueRatio = max(queueSheetBehavior.calculateSlideOffset(), 0f) - val outRatio = 1 - playbackRatio + val outPlaybackRatio = 1 - playbackRatio val halfOutRatio = min(playbackRatio * 2, 1f) val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2 val halfOutQueueRatio = min(queueRatio * 2, 1f) val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2 binding.exploreNavHost.apply { - alpha = outRatio + alpha = outPlaybackRatio isInvisible = alpha == 0f } - binding.playbackSheet.translationZ = 3f * outRatio - playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outRatio * 255).toInt() + binding.playbackSheet.translationZ = 3f * outPlaybackRatio + playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() binding.playbackBarFragment.apply { alpha = max(1 - halfOutRatio, halfInQueueRatio) @@ -156,7 +156,10 @@ class MainFragment : isInvisible = alpha == 0f } - binding.queueFragment.alpha = queueRatio + binding.queueFragment.apply { + alpha = queueRatio + isInvisible = alpha == 0f + } playbackSheetBehavior.isDraggable = playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN && diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 6fa4da716..7d4afef3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -59,9 +59,8 @@ class PlaybackBarFragment : ViewBindingFragment() { // Load the track color in manually as it's unclear whether the track actually supports // using a ColorStateList in the resources - binding.playbackProgressBar.apply { - trackColor = requireContext().getColorStateListSafe(R.color.sel_track).defaultColor - } + binding.playbackProgressBar.trackColor = + requireContext().getColorStateListSafe(R.color.sel_track).defaultColor binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index 442ce1064..360a2a72f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -22,10 +22,6 @@ import android.view.animation.AccelerateDecelerateInterpolator import androidx.core.view.isInvisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.sign import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimenSafe import org.oxycblt.auxio.util.logD @@ -53,27 +49,6 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe } } - override fun interpolateOutOfBoundsScroll( - recyclerView: RecyclerView, - viewSize: Int, - viewSizeOutOfBounds: Int, - totalSize: Int, - msSinceStartScroll: Long - ): Int { - // Fix to make QueueFragment scroll slower 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 = - max( - MINIMUM_INITIAL_DRAG_VELOCITY, - min(abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)) - - return clampedAbsVelocity * sign(viewSizeOutOfBounds.toDouble()).toInt() - } - override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, 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 38d078b05..8d2b10e5a 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 @@ -83,9 +83,11 @@ class QueueFragment : ViewBindingFragment(), QueueItemList .findLastCompletelyVisibleItemPosition() if (instructions.scrollTo !in indices) { - requireBinding().queueRecycler.scrollToPosition(instructions.scrollTo) + binding.queueRecycler.scrollToPosition(instructions.scrollTo) } } + + queueModel.finishInstructions() } else { queueAdapter.data.submitList(queue) } 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 c2da5ba27..abfd580a0 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 @@ -18,6 +18,7 @@ package org.oxycblt.auxio.playback.queue import androidx.lifecycle.ViewModel +import kotlin.math.min import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.MusicParent @@ -80,7 +81,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { } override fun onIndexMoved(index: Int) { - instructions = QueueInstructions(false, index + 1) + instructions = QueueInstructions(false, min(index + 1, playbackManager.queue.lastIndex)) _queue.value = generateQueue(index, playbackManager.queue) } @@ -90,12 +91,12 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { } override fun onQueueReworked(index: Int, queue: List) { - instructions = QueueInstructions(true, index + 1) + instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) _queue.value = generateQueue(index, queue) } override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { - instructions = QueueInstructions(true, index + 1) + instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex)) _queue.value = generateQueue(index, queue) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index 1e20eda6a..a573bb948 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -25,6 +25,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.bottomsheet.NeoBottomSheetBehavior import kotlin.math.abs import org.oxycblt.auxio.util.coordinatorLayoutBehavior +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -33,6 +34,7 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri private var lastInsets: WindowInsets? = null private var dep: View? = null private var setup: Boolean = false + private var lastConsumed: Int? = null override fun onMeasureChild( parent: CoordinatorLayout, @@ -42,32 +44,37 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri parentHeightMeasureSpec: Int, heightUsed: Int ): Boolean { - return measureContent(parent, child, dep ?: return false) + val dep = dep ?: return false + val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior + val consumed = behavior.calculateConsumedByBar() + if (consumed == Int.MIN_VALUE) { + return false + } + + measureContent(parent, child, consumed) + + return true } override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { super.onLayoutChild(parent, child, layoutDirection) - child.layout(0, 0, child.measuredWidth, child.measuredHeight) + layoutContent(child) if (!setup) { - child.setOnApplyWindowInsetsListener { _, insets -> + + child.setOnApplyWindowInsetsListener { v, insets -> lastInsets = insets - val dep = dep ?: return@setOnApplyWindowInsetsListener insets - - val bars = insets.systemBarInsetsCompat val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior - - val offset = behavior.calculateSlideOffset() - if (behavior.peekHeight < 0 || offset == Float.MIN_VALUE) { + val consumed = behavior.calculateConsumedByBar() + if (consumed == Int.MIN_VALUE) { return@setOnApplyWindowInsetsListener insets } - val adjustedBottomInset = - (bars.bottom - behavior.calculateConsumedByBar()).coerceAtLeast(0) + val bars = insets.systemBarInsetsCompat insets.replaceSystemBarInsetsCompat( - bars.left, bars.top, bars.right, adjustedBottomInset) + bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0)) } setup = true @@ -76,27 +83,28 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri return true } - private fun measureContent(parent: View, child: View, dep: View): Boolean { - val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior - - val offset = behavior.calculateSlideOffset() - if (behavior.peekHeight < 0 || offset == Float.MIN_VALUE) { - return false - } - + private fun measureContent(parent: View, child: View, consumed: Int) { val contentWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY) val contentHeightSpec = View.MeasureSpec.makeMeasureSpec( - parent.measuredHeight - behavior.calculateConsumedByBar(), View.MeasureSpec.EXACTLY) + parent.measuredHeight - consumed, View.MeasureSpec.EXACTLY) child.measure(contentWidthSpec, contentHeightSpec) + } - return true + private fun layoutContent(child: View) { + logD("Measure") + + child.layout(0, 0, child.measuredWidth, child.measuredHeight) } private fun NeoBottomSheetBehavior<*>.calculateConsumedByBar(): Int { val offset = calculateSlideOffset() + if (offset == Float.MIN_VALUE || peekHeight < 0) { + return Int.MIN_VALUE + } + return if (offset >= 0) { peekHeight } else { @@ -118,13 +126,30 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri child: V, dependency: View ): Boolean { + logD("Dependent view changed $child") + val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior - if (behavior.calculateSlideOffset() > 0) { + val consumed = behavior.calculateConsumedByBar() + if (consumed < Int.MIN_VALUE) { return false } - lastInsets?.let(child::dispatchApplyWindowInsets) - return measureContent(parent, child, dependency) && - onLayoutChild(parent, child, parent.layoutDirection) + if (consumed != lastConsumed) { + logD("Dependent view changed important $child") + + lastConsumed = consumed + + val insets = lastInsets + if (insets != null) { + child.dispatchApplyWindowInsets(insets) + } + + lastInsets?.let(child::dispatchApplyWindowInsets) + measureContent(parent, child, consumed) + layoutContent(child) + return true + } + + return false } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/EdgeRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/EdgeRecyclerView.kt index dc4d673a5..a38f044f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/EdgeRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/EdgeRecyclerView.kt @@ -36,11 +36,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr init { // Prevent children from being clipped by window insets clipToPadding = false + setHasFixedSize(true) } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - setHasFixedSize(true) + final override fun setHasFixedSize(hasFixedSize: Boolean) { + super.setHasFixedSize(hasFixedSize) } override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {