summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Anton Potapov <apotapov@google.com> 2024-03-21 11:22:27 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-03-21 11:22:27 +0000
commit762ac9e00e142a32f1940b333cb7d2ff8126ea70 (patch)
tree21ef21bf735f1befe38a47dd248e07adf74b45fa
parentafdca93e70f2c7fbfce162d3868acdcff3f407eb (diff)
parentcdc5ee33b943eb03a8787ff4de2dc5629a845203 (diff)
Merge "Rework VolumeSlider content to accomodate enough space for disabledMessage animation" into main
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt55
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt152
2 files changed, 179 insertions, 28 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 248dfeee2281..d31064ae23b3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -17,17 +17,19 @@
package com.android.systemui.volume.panel.component.volume.ui.composable
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -52,16 +54,15 @@ fun VolumeSlider(
modifier: Modifier = Modifier,
sliderColors: PlatformSliderColors,
) {
- val value by
- animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+ val value by valueState(state)
PlatformSlider(
modifier =
modifier.clearAndSetSemantics {
if (!state.isEnabled) disabled()
contentDescription = state.label
- // provide a not animated value to the a11y because it fails to announce the settled
- // value when it changes rapidly.
+ // provide a not animated value to the a11y because it fails to announce the
+ // settled value when it changes rapidly.
progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
setProgress { targetValue ->
val targetDirection =
@@ -99,32 +100,30 @@ fun VolumeSlider(
},
colors = sliderColors,
label = {
- Column(modifier = Modifier) {
- Text(
- modifier = Modifier.basicMarquee(),
- text = state.label,
- style = MaterialTheme.typography.titleMedium,
- color = LocalContentColor.current,
- maxLines = 1,
- )
-
- if (!state.isEnabled) {
- state.disabledMessage?.let { message ->
- Text(
- modifier = Modifier.basicMarquee(),
- text = message,
- style = MaterialTheme.typography.bodySmall,
- color = LocalContentColor.current,
- maxLines = 1,
- )
- }
- }
- }
+ VolumeSliderContent(
+ modifier = Modifier,
+ label = state.label,
+ isEnabled = state.isEnabled,
+ disabledMessage = state.disabledMessage,
+ )
}
)
}
@Composable
+private fun valueState(state: SliderState): State<Float> {
+ var prevState by remember { mutableStateOf(state) }
+ // Don't animate slider value when receive the first value and when changing isEnabled state
+ val shouldSkipAnimation =
+ prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled
+ val value =
+ if (shouldSkipAnimation) mutableFloatStateOf(state.value)
+ else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+ prevState = state
+ return value
+}
+
+@Composable
private fun SliderIcon(
icon: Icon,
onIconTapped: () -> Unit,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
new file mode 100644
index 000000000000..6b9af239eb6f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.volume.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlinx.coroutines.launch
+
+private enum class VolumeSliderContentComponent {
+ Label,
+ DisabledMessage,
+}
+
+/** Shows label of the [VolumeSlider]. Also shows [disabledMessage] when not [isEnabled]. */
+@Composable
+fun VolumeSliderContent(
+ label: String,
+ isEnabled: Boolean,
+ disabledMessage: String?,
+ modifier: Modifier = Modifier,
+) {
+ Layout(
+ modifier = modifier.animateContentHeight(),
+ content = {
+ Text(
+ modifier = Modifier.layoutId(VolumeSliderContentComponent.Label).basicMarquee(),
+ text = label,
+ style = MaterialTheme.typography.titleMedium,
+ color = LocalContentColor.current,
+ maxLines = 1,
+ )
+
+ disabledMessage?.let { message ->
+ AnimatedVisibility(
+ modifier = Modifier.layoutId(VolumeSliderContentComponent.DisabledMessage),
+ visible = !isEnabled,
+ enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
+ exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
+ ) {
+ Text(
+ modifier = Modifier.basicMarquee(),
+ text = message,
+ style = MaterialTheme.typography.bodySmall,
+ color = LocalContentColor.current,
+ maxLines = 1,
+ )
+ }
+ }
+ },
+ measurePolicy = VolumeSliderContentMeasurePolicy(isEnabled)
+ )
+}
+
+/**
+ * Uses [VolumeSliderContentComponent.Label] width when [isEnabled] and max available width
+ * otherwise. This ensures that the slider always have the correct measurement to position the
+ * content.
+ */
+private class VolumeSliderContentMeasurePolicy(private val isEnabled: Boolean) : MeasurePolicy {
+
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ val labelPlaceable =
+ measurables
+ .fastFirst { it.layoutId == VolumeSliderContentComponent.Label }
+ .measure(constraints)
+ val layoutWidth: Int = constraints.maxWidth
+ val fullLayoutWidth: Int =
+ if (isEnabled) {
+ // PlatformSlider uses half of the available space for the enabled state.
+ // This is using it to allow disabled message to take whole space when animating to
+ // prevent it from jumping left to right
+ layoutWidth * 2
+ } else {
+ layoutWidth
+ }
+
+ val disabledMessagePlaceable =
+ measurables
+ .fastFirstOrNull { it.layoutId == VolumeSliderContentComponent.DisabledMessage }
+ ?.measure(constraints.copy(maxWidth = fullLayoutWidth))
+
+ val layoutHeight = labelPlaceable.height + (disabledMessagePlaceable?.height ?: 0)
+ return layout(layoutWidth, layoutHeight) {
+ labelPlaceable.placeRelative(0, 0, 0f)
+ disabledMessagePlaceable?.placeRelative(0, labelPlaceable.height, 0f)
+ }
+ }
+}
+
+/** Animates composable height changes. */
+@Composable
+private fun Modifier.animateContentHeight(): Modifier {
+ var heightAnimation by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) }
+ val coroutineScope = rememberCoroutineScope()
+ return layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ val currentAnimation = heightAnimation
+ val anim =
+ if (currentAnimation == null) {
+ Animatable(placeable.height, Int.VectorConverter).also { heightAnimation = it }
+ } else {
+ coroutineScope.launch { currentAnimation.animateTo(placeable.height) }
+ currentAnimation
+ }
+ layout(placeable.width, anim.value) { placeable.place(0, 0) }
+ }
+}