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.
This commit is contained in:
parent
54be8dc2dc
commit
c3d8509069
9 changed files with 74 additions and 68 deletions
|
@ -13,6 +13,7 @@ at the cost of longer loading times
|
||||||
- Queue can now be swiped up [#92]
|
- Queue can now be swiped up [#92]
|
||||||
- Playing song is now shown in queue [#92]
|
- Playing song is now shown in queue [#92]
|
||||||
- Added ability to play songs from queue [#92]
|
- Added ability to play songs from queue [#92]
|
||||||
|
- Added ability to see previous songs in queue
|
||||||
- Added Last Added sorting
|
- Added Last Added sorting
|
||||||
- Search now takes sort tags and file names in account [#184]
|
- Search now takes sort tags and file names in account [#184]
|
||||||
- Added option to clear playback state in settings
|
- Added option to clear playback state in settings
|
||||||
|
|
|
@ -11,8 +11,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId namespace
|
applicationId namespace
|
||||||
versionName "2.5.0"
|
versionName "2.6.0-beta"
|
||||||
versionCode 18
|
versionCode 19
|
||||||
|
|
||||||
// API 33 is still busted, waiting until the XML element issue is fixed
|
// API 33 is still busted, waiting until the XML element issue is fixed
|
||||||
// noinspection OldTargetApi
|
// noinspection OldTargetApi
|
||||||
|
|
|
@ -131,19 +131,19 @@ class MainFragment :
|
||||||
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
||||||
val queueRatio = max(queueSheetBehavior.calculateSlideOffset(), 0f)
|
val queueRatio = max(queueSheetBehavior.calculateSlideOffset(), 0f)
|
||||||
|
|
||||||
val outRatio = 1 - playbackRatio
|
val outPlaybackRatio = 1 - playbackRatio
|
||||||
val halfOutRatio = min(playbackRatio * 2, 1f)
|
val halfOutRatio = min(playbackRatio * 2, 1f)
|
||||||
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
|
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
|
||||||
val halfOutQueueRatio = min(queueRatio * 2, 1f)
|
val halfOutQueueRatio = min(queueRatio * 2, 1f)
|
||||||
val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2
|
val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2
|
||||||
|
|
||||||
binding.exploreNavHost.apply {
|
binding.exploreNavHost.apply {
|
||||||
alpha = outRatio
|
alpha = outPlaybackRatio
|
||||||
isInvisible = alpha == 0f
|
isInvisible = alpha == 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSheet.translationZ = 3f * outRatio
|
binding.playbackSheet.translationZ = 3f * outPlaybackRatio
|
||||||
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outRatio * 255).toInt()
|
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
|
||||||
|
|
||||||
binding.playbackBarFragment.apply {
|
binding.playbackBarFragment.apply {
|
||||||
alpha = max(1 - halfOutRatio, halfInQueueRatio)
|
alpha = max(1 - halfOutRatio, halfInQueueRatio)
|
||||||
|
@ -156,7 +156,10 @@ class MainFragment :
|
||||||
isInvisible = alpha == 0f
|
isInvisible = alpha == 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.queueFragment.alpha = queueRatio
|
binding.queueFragment.apply {
|
||||||
|
alpha = queueRatio
|
||||||
|
isInvisible = alpha == 0f
|
||||||
|
}
|
||||||
|
|
||||||
playbackSheetBehavior.isDraggable =
|
playbackSheetBehavior.isDraggable =
|
||||||
playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN &&
|
playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN &&
|
||||||
|
|
|
@ -59,9 +59,8 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
|
|
||||||
// Load the track color in manually as it's unclear whether the track actually supports
|
// Load the track color in manually as it's unclear whether the track actually supports
|
||||||
// using a ColorStateList in the resources
|
// using a ColorStateList in the resources
|
||||||
binding.playbackProgressBar.apply {
|
binding.playbackProgressBar.trackColor =
|
||||||
trackColor = requireContext().getColorStateListSafe(R.color.sel_track).defaultColor
|
requireContext().getColorStateListSafe(R.color.sel_track).defaultColor
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
|
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,6 @@ import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.R
|
||||||
import org.oxycblt.auxio.util.getDimenSafe
|
import org.oxycblt.auxio.util.getDimenSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
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(
|
override fun onChildDraw(
|
||||||
c: Canvas,
|
c: Canvas,
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
|
|
|
@ -83,9 +83,11 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
.findLastCompletelyVisibleItemPosition()
|
.findLastCompletelyVisibleItemPosition()
|
||||||
|
|
||||||
if (instructions.scrollTo !in indices) {
|
if (instructions.scrollTo !in indices) {
|
||||||
requireBinding().queueRecycler.scrollToPosition(instructions.scrollTo)
|
binding.queueRecycler.scrollToPosition(instructions.scrollTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queueModel.finishInstructions()
|
||||||
} else {
|
} else {
|
||||||
queueAdapter.data.submitList(queue)
|
queueAdapter.data.submitList(queue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
@ -80,7 +81,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexMoved(index: Int) {
|
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)
|
_queue.value = generateQueue(index, playbackManager.queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,12 +91,12 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueReworked(index: Int, queue: List<Song>) {
|
override fun onQueueReworked(index: Int, queue: List<Song>) {
|
||||||
instructions = QueueInstructions(true, index + 1)
|
instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex))
|
||||||
_queue.value = generateQueue(index, queue)
|
_queue.value = generateQueue(index, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||||
instructions = QueueInstructions(true, index + 1)
|
instructions = QueueInstructions(true, min(index + 1, playbackManager.queue.lastIndex))
|
||||||
_queue.value = generateQueue(index, queue)
|
_queue.value = generateQueue(index, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var dep: View? = null
|
private var dep: View? = null
|
||||||
private var setup: Boolean = false
|
private var setup: Boolean = false
|
||||||
|
private var lastConsumed: Int? = null
|
||||||
|
|
||||||
override fun onMeasureChild(
|
override fun onMeasureChild(
|
||||||
parent: CoordinatorLayout,
|
parent: CoordinatorLayout,
|
||||||
|
@ -42,32 +44,37 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
parentHeightMeasureSpec: Int,
|
parentHeightMeasureSpec: Int,
|
||||||
heightUsed: Int
|
heightUsed: Int
|
||||||
): Boolean {
|
): 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 {
|
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
|
||||||
super.onLayoutChild(parent, child, layoutDirection)
|
super.onLayoutChild(parent, child, layoutDirection)
|
||||||
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
|
layoutContent(child)
|
||||||
|
|
||||||
if (!setup) {
|
if (!setup) {
|
||||||
child.setOnApplyWindowInsetsListener { _, insets ->
|
|
||||||
|
child.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
lastInsets = insets
|
lastInsets = insets
|
||||||
|
|
||||||
val dep = dep ?: return@setOnApplyWindowInsetsListener insets
|
val dep = dep ?: return@setOnApplyWindowInsetsListener insets
|
||||||
|
|
||||||
val bars = insets.systemBarInsetsCompat
|
|
||||||
val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior
|
val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior
|
||||||
|
val consumed = behavior.calculateConsumedByBar()
|
||||||
val offset = behavior.calculateSlideOffset()
|
if (consumed == Int.MIN_VALUE) {
|
||||||
if (behavior.peekHeight < 0 || offset == Float.MIN_VALUE) {
|
|
||||||
return@setOnApplyWindowInsetsListener insets
|
return@setOnApplyWindowInsetsListener insets
|
||||||
}
|
}
|
||||||
|
|
||||||
val adjustedBottomInset =
|
val bars = insets.systemBarInsetsCompat
|
||||||
(bars.bottom - behavior.calculateConsumedByBar()).coerceAtLeast(0)
|
|
||||||
|
|
||||||
insets.replaceSystemBarInsetsCompat(
|
insets.replaceSystemBarInsetsCompat(
|
||||||
bars.left, bars.top, bars.right, adjustedBottomInset)
|
bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setup = true
|
setup = true
|
||||||
|
@ -76,27 +83,28 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun measureContent(parent: View, child: View, dep: View): Boolean {
|
private fun measureContent(parent: View, child: View, consumed: Int) {
|
||||||
val behavior = dep.coordinatorLayoutBehavior as NeoBottomSheetBehavior
|
|
||||||
|
|
||||||
val offset = behavior.calculateSlideOffset()
|
|
||||||
if (behavior.peekHeight < 0 || offset == Float.MIN_VALUE) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentWidthSpec =
|
val contentWidthSpec =
|
||||||
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
|
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
|
||||||
val contentHeightSpec =
|
val contentHeightSpec =
|
||||||
View.MeasureSpec.makeMeasureSpec(
|
View.MeasureSpec.makeMeasureSpec(
|
||||||
parent.measuredHeight - behavior.calculateConsumedByBar(), View.MeasureSpec.EXACTLY)
|
parent.measuredHeight - consumed, View.MeasureSpec.EXACTLY)
|
||||||
|
|
||||||
child.measure(contentWidthSpec, contentHeightSpec)
|
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 {
|
private fun NeoBottomSheetBehavior<*>.calculateConsumedByBar(): Int {
|
||||||
val offset = calculateSlideOffset()
|
val offset = calculateSlideOffset()
|
||||||
|
if (offset == Float.MIN_VALUE || peekHeight < 0) {
|
||||||
|
return Int.MIN_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
return if (offset >= 0) {
|
return if (offset >= 0) {
|
||||||
peekHeight
|
peekHeight
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,13 +126,30 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
child: V,
|
child: V,
|
||||||
dependency: View
|
dependency: View
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
logD("Dependent view changed $child")
|
||||||
|
|
||||||
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
|
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
|
||||||
if (behavior.calculateSlideOffset() > 0) {
|
val consumed = behavior.calculateConsumedByBar()
|
||||||
|
if (consumed < Int.MIN_VALUE) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (consumed != lastConsumed) {
|
||||||
|
logD("Dependent view changed important $child")
|
||||||
|
|
||||||
|
lastConsumed = consumed
|
||||||
|
|
||||||
|
val insets = lastInsets
|
||||||
|
if (insets != null) {
|
||||||
|
child.dispatchApplyWindowInsets(insets)
|
||||||
|
}
|
||||||
|
|
||||||
lastInsets?.let(child::dispatchApplyWindowInsets)
|
lastInsets?.let(child::dispatchApplyWindowInsets)
|
||||||
return measureContent(parent, child, dependency) &&
|
measureContent(parent, child, consumed)
|
||||||
onLayoutChild(parent, child, parent.layoutDirection)
|
layoutContent(child)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
init {
|
init {
|
||||||
// Prevent children from being clipped by window insets
|
// Prevent children from being clipped by window insets
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
|
setHasFixedSize(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
final override fun setHasFixedSize(hasFixedSize: Boolean) {
|
||||||
super.onAttachedToWindow()
|
super.setHasFixedSize(hasFixedSize)
|
||||||
setHasFixedSize(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
|
Loading…
Reference in a new issue