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