• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.EnterTransition
21 import androidx.compose.animation.ExitTransition
22 import androidx.compose.animation.core.AnimationSpec
23 import androidx.compose.animation.core.animateDpAsState
24 import androidx.compose.animation.core.tween
25 import androidx.compose.animation.expandVertically
26 import androidx.compose.animation.fadeIn
27 import androidx.compose.animation.fadeOut
28 import androidx.compose.animation.scaleIn
29 import androidx.compose.animation.scaleOut
30 import androidx.compose.animation.shrinkVertically
31 import androidx.compose.foundation.layout.Box
32 import androidx.compose.foundation.layout.Column
33 import androidx.compose.foundation.layout.RowScope
34 import androidx.compose.foundation.layout.fillMaxWidth
35 import androidx.compose.foundation.layout.padding
36 import androidx.compose.foundation.layout.size
37 import androidx.compose.foundation.shape.RoundedCornerShape
38 import androidx.compose.material3.Icon
39 import androidx.compose.material3.IconButton
40 import androidx.compose.material3.IconButtonDefaults
41 import androidx.compose.material3.MaterialTheme
42 import androidx.compose.runtime.Composable
43 import androidx.compose.runtime.State
44 import androidx.compose.runtime.getValue
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.Modifier
47 import androidx.compose.ui.res.painterResource
48 import androidx.compose.ui.res.stringResource
49 import androidx.compose.ui.semantics.Role
50 import androidx.compose.ui.semantics.role
51 import androidx.compose.ui.semantics.semantics
52 import androidx.compose.ui.semantics.stateDescription
53 import androidx.compose.ui.unit.Dp
54 import androidx.compose.ui.unit.dp
55 import androidx.lifecycle.compose.collectAsStateWithLifecycle
56 import com.android.compose.PlatformIconButton
57 import com.android.compose.PlatformSliderColors
58 import com.android.compose.modifiers.padding
59 import com.android.compose.modifiers.thenIf
60 import com.android.systemui.Flags
61 import com.android.systemui.res.R
62 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
63 
64 private const val EXPAND_DURATION_MILLIS = 500
65 private const val COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS = 350
66 private const val COLLAPSE_DURATION_MILLIS = 300
67 private const val EXPAND_BUTTON_ANIMATION_DURATION_MILLIS = 350
68 private const val TOP_SLIDER_ANIMATION_DURATION_MILLIS = 400
69 private const val SHRINK_FRACTION = 0.55f
70 private const val SCALE_FRACTION = 0.9f
71 private const val EXPAND_BUTTON_SCALE = 0.8f
72 
73 @Composable
74 fun ColumnVolumeSliders(
75     viewModels: List<SliderViewModel>,
76     isExpanded: Boolean,
77     onExpandedChanged: (Boolean) -> Unit,
78     sliderColors: PlatformSliderColors,
79     isExpandable: Boolean,
80     modifier: Modifier = Modifier,
81 ) {
82     require(viewModels.isNotEmpty())
83     Column(modifier = modifier) {
84         Box(modifier = Modifier.fillMaxWidth()) {
85             val sliderViewModel: SliderViewModel = viewModels.first()
86             val sliderState by viewModels.first().slider.collectAsStateWithLifecycle()
87             val sliderPadding by topSliderPadding(isExpandable)
88 
89             VolumeSlider(
90                 modifier =
91                     Modifier.thenIf(!Flags.volumeRedesign()) {
92                             Modifier.padding(end = { sliderPadding.roundToPx() })
93                         }
94                         .fillMaxWidth(),
95                 state = sliderState,
96                 onValueChange = { newValue: Float ->
97                     sliderViewModel.onValueChanged(sliderState, newValue)
98                 },
99                 onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
100                 onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
101                 sliderColors = sliderColors,
102                 hapticsViewModelFactory = sliderViewModel.getSliderHapticsViewModelFactory(),
103                 button =
104                     if (Flags.volumeRedesign()) {
105                         {
106                             ExpandButton(
107                                 isExpanded = isExpanded,
108                                 isExpandable = isExpandable,
109                                 onExpandedChanged = onExpandedChanged,
110                             )
111                         }
112                     } else {
113                         null
114                     },
115             )
116 
117             if (!Flags.volumeRedesign()) {
118                 ExpandButtonLegacy(
119                     modifier = Modifier.align(Alignment.CenterEnd),
120                     isExpanded = isExpanded,
121                     isExpandable = isExpandable,
122                     onExpandedChanged = onExpandedChanged,
123                     sliderColors = sliderColors,
124                 )
125             }
126         }
127         AnimatedVisibility(
128             visible = isExpanded || !isExpandable,
129             label = "CollapsableSliders",
130             enter =
131                 expandVertically(animationSpec = tween(durationMillis = EXPAND_DURATION_MILLIS)),
132             exit =
133                 shrinkVertically(animationSpec = tween(durationMillis = COLLAPSE_DURATION_MILLIS)),
134         ) {
135             // This box allows sliders to slide towards top when the container is shrinking and
136             // slide from top when the container is expanding.
137             Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) {
138                 Column {
139                     for (index in 1..viewModels.lastIndex) {
140                         val sliderViewModel: SliderViewModel = viewModels[index]
141                         val sliderState by sliderViewModel.slider.collectAsStateWithLifecycle()
142 
143                         VolumeSlider(
144                             modifier =
145                                 Modifier.fillMaxWidth()
146                                     .animateEnterExit(
147                                         enter =
148                                             enterTransition(
149                                                 index = index,
150                                                 totalCount = viewModels.size,
151                                             ),
152                                         exit =
153                                             exitTransition(
154                                                 index = index,
155                                                 totalCount = viewModels.size,
156                                             ),
157                                     )
158                                     .padding(top = if (Flags.volumeRedesign()) 4.dp else 16.dp),
159                             state = sliderState,
160                             onValueChange = { newValue: Float ->
161                                 sliderViewModel.onValueChanged(sliderState, newValue)
162                             },
163                             onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
164                             onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
165                             sliderColors = sliderColors,
166                             hapticsViewModelFactory =
167                                 sliderViewModel.getSliderHapticsViewModelFactory(),
168                         )
169                     }
170                 }
171             }
172         }
173     }
174 }
175 
176 @Composable
ExpandButtonLegacynull177 private fun ExpandButtonLegacy(
178     isExpanded: Boolean,
179     isExpandable: Boolean,
180     onExpandedChanged: (Boolean) -> Unit,
181     sliderColors: PlatformSliderColors,
182     modifier: Modifier = Modifier,
183 ) {
184     val expandButtonStateDescription =
185         if (isExpanded) {
186             stringResource(R.string.volume_panel_expanded_sliders)
187         } else {
188             stringResource(R.string.volume_panel_collapsed_sliders)
189         }
190     AnimatedVisibility(
191         modifier = modifier,
192         visible = isExpandable,
193         enter = expandButtonEnterTransition(),
194         exit = expandButtonExitTransition(),
195     ) {
196         IconButton(
197             modifier =
198                 Modifier.size(64.dp).semantics {
199                     role = Role.Switch
200                     stateDescription = expandButtonStateDescription
201                 },
202             onClick = { onExpandedChanged(!isExpanded) },
203             colors =
204                 IconButtonDefaults.filledIconButtonColors(
205                     containerColor = sliderColors.indicatorColor,
206                     contentColor = sliderColors.iconColor,
207                 ),
208         ) {
209             Icon(
210                 painter =
211                     painterResource(
212                         if (isExpanded) {
213                             R.drawable.ic_filled_arrow_down
214                         } else {
215                             R.drawable.ic_filled_arrow_up
216                         }
217                     ),
218                 contentDescription = null,
219             )
220         }
221     }
222 }
223 
224 @Composable
ExpandButtonnull225 private fun RowScope.ExpandButton(
226     isExpanded: Boolean,
227     isExpandable: Boolean,
228     onExpandedChanged: (Boolean) -> Unit,
229     modifier: Modifier = Modifier,
230 ) {
231     val expandButtonStateDescription =
232         if (isExpanded) {
233             stringResource(R.string.volume_panel_expanded_sliders)
234         } else {
235             stringResource(R.string.volume_panel_collapsed_sliders)
236         }
237     AnimatedVisibility(
238         modifier = modifier,
239         visible = isExpandable,
240         enter = expandButtonEnterTransition(),
241         exit = expandButtonExitTransition(),
242     ) {
243         PlatformIconButton(
244             modifier =
245                 Modifier.size(40.dp).semantics {
246                     role = Role.Switch
247                     stateDescription = expandButtonStateDescription
248                 },
249             onClick = { onExpandedChanged(!isExpanded) },
250             colors =
251                 IconButtonDefaults.iconButtonColors(
252                     containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
253                     contentColor = MaterialTheme.colorScheme.onSurface,
254                 ),
255             shape = RoundedCornerShape(12.dp),
256             iconResource =
257                 if (isExpanded) {
258                     R.drawable.ic_arrow_down_24dp
259                 } else {
260                     R.drawable.ic_arrow_up_24dp
261                 },
262             contentDescription = null,
263         )
264     }
265 }
266 
enterTransitionnull267 private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
268     val enterDelay = ((totalCount - index + 1) * 10).coerceAtLeast(0)
269     val enterDuration = (EXPAND_DURATION_MILLIS - enterDelay).coerceAtLeast(100)
270     return scaleIn(
271         initialScale = SCALE_FRACTION,
272         animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
273     ) +
274         expandVertically(
275             initialHeight = { (it * SHRINK_FRACTION).toInt() },
276             animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
277             clip = false,
278         ) +
279         fadeIn(animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay))
280 }
281 
exitTransitionnull282 private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
283     val exitDuration = (COLLAPSE_DURATION_MILLIS - (totalCount - index + 1) * 10).coerceAtLeast(100)
284     return scaleOut(
285         targetScale = SCALE_FRACTION,
286         animationSpec = tween(durationMillis = exitDuration),
287     ) +
288         shrinkVertically(
289             targetHeight = { (it * SHRINK_FRACTION).toInt() },
290             animationSpec = tween(durationMillis = exitDuration),
291             clip = false,
292         ) +
293         fadeOut(animationSpec = tween(durationMillis = exitDuration))
294 }
295 
expandButtonEnterTransitionnull296 private fun expandButtonEnterTransition(): EnterTransition {
297     return fadeIn(
298         tween(
299             delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
300             durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
301         )
302     ) +
303         scaleIn(
304             animationSpec =
305                 tween(
306                     delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
307                     durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
308                 ),
309             initialScale = EXPAND_BUTTON_SCALE,
310         )
311 }
312 
expandButtonExitTransitionnull313 private fun expandButtonExitTransition(): ExitTransition {
314     return fadeOut(
315         tween(
316             delayMillis = EXPAND_DURATION_MILLIS,
317             durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
318         )
319     ) +
320         scaleOut(
321             animationSpec =
322                 tween(
323                     delayMillis = EXPAND_DURATION_MILLIS,
324                     durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
325                 ),
326             targetScale = EXPAND_BUTTON_SCALE,
327         )
328 }
329 
330 @Composable
topSliderPaddingnull331 private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
332     val animationSpec: AnimationSpec<Dp> =
333         if (isExpandable) {
334             tween(
335                 delayMillis = COLLAPSE_DURATION_MILLIS,
336                 durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
337             )
338         } else {
339             tween(
340                 delayMillis = EXPAND_DURATION_MILLIS,
341                 durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
342             )
343         }
344     return animateDpAsState(
345         targetValue =
346             if (isExpandable) {
347                 72.dp
348             } else {
349                 0.dp
350             },
351         animationSpec = animationSpec,
352         label = "TopVolumeSliderPadding",
353     )
354 }
355