diff options
| author | 2024-03-21 11:22:27 +0000 | |
|---|---|---|
| committer | 2024-03-21 11:22:27 +0000 | |
| commit | 762ac9e00e142a32f1940b333cb7d2ff8126ea70 (patch) | |
| tree | 21ef21bf735f1befe38a47dd248e07adf74b45fa | |
| parent | afdca93e70f2c7fbfce162d3868acdcff3f407eb (diff) | |
| parent | cdc5ee33b943eb03a8787ff4de2dc5629a845203 (diff) | |
Merge "Rework VolumeSlider content to accomodate enough space for disabledMessage animation" into main
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) } + } +} |