1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.volume.panel.component.volume.ui.composable
18
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.core.Animatable
21 import androidx.compose.animation.core.AnimationVector1D
22 import androidx.compose.animation.core.VectorConverter
23 import androidx.compose.animation.expandVertically
24 import androidx.compose.animation.fadeIn
25 import androidx.compose.animation.fadeOut
26 import androidx.compose.animation.shrinkVertically
27 import androidx.compose.foundation.basicMarquee
28 import androidx.compose.material3.LocalContentColor
29 import androidx.compose.material3.MaterialTheme
30 import androidx.compose.material3.Text
31 import androidx.compose.runtime.Composable
32 import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.mutableStateOf
34 import androidx.compose.runtime.remember
35 import androidx.compose.runtime.rememberCoroutineScope
36 import androidx.compose.runtime.setValue
37 import androidx.compose.ui.Alignment
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.layout.Layout
40 import androidx.compose.ui.layout.Measurable
41 import androidx.compose.ui.layout.MeasurePolicy
42 import androidx.compose.ui.layout.MeasureResult
43 import androidx.compose.ui.layout.MeasureScope
44 import androidx.compose.ui.layout.layout
45 import androidx.compose.ui.layout.layoutId
46 import androidx.compose.ui.unit.Constraints
47 import androidx.compose.ui.util.fastFirst
48 import androidx.compose.ui.util.fastFirstOrNull
49 import kotlinx.coroutines.launch
50
51 private enum class VolumeSliderContentComponent {
52 Label,
53 DisabledMessage,
54 }
55
56 /** Shows label of the [VolumeSlider]. Also shows [disabledMessage] when not [isEnabled]. */
57 @Composable
VolumeSliderContentnull58 fun VolumeSliderContent(
59 label: String,
60 isEnabled: Boolean,
61 disabledMessage: String?,
62 modifier: Modifier = Modifier,
63 ) {
64 Layout(
65 modifier = modifier.animateContentHeight(),
66 content = {
67 Text(
68 modifier = Modifier.layoutId(VolumeSliderContentComponent.Label).basicMarquee(),
69 text = label,
70 style = MaterialTheme.typography.titleMedium,
71 color = LocalContentColor.current,
72 maxLines = 1,
73 )
74
75 disabledMessage?.let { message ->
76 AnimatedVisibility(
77 modifier = Modifier.layoutId(VolumeSliderContentComponent.DisabledMessage),
78 visible = !isEnabled,
79 enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
80 exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
81 ) {
82 Text(
83 modifier = Modifier.basicMarquee(),
84 text = message,
85 style = MaterialTheme.typography.bodySmall,
86 color = LocalContentColor.current,
87 maxLines = 1,
88 )
89 }
90 }
91 },
92 measurePolicy = VolumeSliderContentMeasurePolicy(isEnabled)
93 )
94 }
95
96 /**
97 * Uses [VolumeSliderContentComponent.Label] width when [isEnabled] and max available width
98 * otherwise. This ensures that the slider always have the correct measurement to position the
99 * content.
100 */
101 private class VolumeSliderContentMeasurePolicy(private val isEnabled: Boolean) : MeasurePolicy {
102
measurenull103 override fun MeasureScope.measure(
104 measurables: List<Measurable>,
105 constraints: Constraints
106 ): MeasureResult {
107 val labelPlaceable =
108 measurables
109 .fastFirst { it.layoutId == VolumeSliderContentComponent.Label }
110 .measure(constraints)
111 val layoutWidth: Int =
112 if (isEnabled) {
113 labelPlaceable.width
114 } else {
115 constraints.maxWidth
116 }
117 val fullLayoutWidth: Int =
118 if (isEnabled) {
119 // PlatformSlider uses half of the available space for the enabled state.
120 // This is using it to allow disabled message to take whole space when animating to
121 // prevent it from jumping left to right
122 constraints.maxWidth * 2
123 } else {
124 constraints.maxWidth
125 }
126
127 val disabledMessagePlaceable =
128 measurables
129 .fastFirstOrNull { it.layoutId == VolumeSliderContentComponent.DisabledMessage }
130 ?.measure(constraints.copy(maxWidth = fullLayoutWidth))
131
132 val layoutHeight = labelPlaceable.height + (disabledMessagePlaceable?.height ?: 0)
133 return layout(layoutWidth, layoutHeight) {
134 labelPlaceable.placeRelative(0, 0, 0f)
135 disabledMessagePlaceable?.placeRelative(0, labelPlaceable.height, 0f)
136 }
137 }
138 }
139
140 /** Animates composable height changes. */
141 @Composable
Modifiernull142 private fun Modifier.animateContentHeight(): Modifier {
143 var heightAnimation by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) }
144 val coroutineScope = rememberCoroutineScope()
145 return layout { measurable, constraints ->
146 val placeable = measurable.measure(constraints)
147 val currentAnimation = heightAnimation
148 val anim =
149 if (currentAnimation == null) {
150 Animatable(placeable.height, Int.VectorConverter).also { heightAnimation = it }
151 } else {
152 coroutineScope.launch { currentAnimation.animateTo(placeable.height) }
153 currentAnimation
154 }
155 layout(placeable.width, anim.value) { placeable.place(0, 0) }
156 }
157 }
158