• 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.animateContentSize
21 import androidx.compose.animation.core.animateFloatAsState
22 import androidx.compose.animation.core.tween
23 import androidx.compose.animation.fadeIn
24 import androidx.compose.animation.fadeOut
25 import androidx.compose.foundation.basicMarquee
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.gestures.Orientation
28 import androidx.compose.foundation.interaction.MutableInteractionSource
29 import androidx.compose.foundation.layout.Arrangement
30 import androidx.compose.foundation.layout.Box
31 import androidx.compose.foundation.layout.Column
32 import androidx.compose.foundation.layout.Row
33 import androidx.compose.foundation.layout.RowScope
34 import androidx.compose.foundation.layout.fillMaxSize
35 import androidx.compose.foundation.layout.fillMaxWidth
36 import androidx.compose.foundation.layout.padding
37 import androidx.compose.foundation.layout.size
38 import androidx.compose.material3.ExperimentalMaterial3Api
39 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
40 import androidx.compose.material3.Icon as MaterialIcon
41 import androidx.compose.material3.MaterialTheme
42 import androidx.compose.material3.SliderDefaults
43 import androidx.compose.material3.Text
44 import androidx.compose.runtime.Composable
45 import androidx.compose.runtime.LaunchedEffect
46 import androidx.compose.runtime.State
47 import androidx.compose.runtime.getValue
48 import androidx.compose.runtime.mutableFloatStateOf
49 import androidx.compose.runtime.mutableStateOf
50 import androidx.compose.runtime.remember
51 import androidx.compose.runtime.setValue
52 import androidx.compose.runtime.snapshotFlow
53 import androidx.compose.ui.Alignment
54 import androidx.compose.ui.Modifier
55 import androidx.compose.ui.res.painterResource
56 import androidx.compose.ui.semantics.CustomAccessibilityAction
57 import androidx.compose.ui.semantics.ProgressBarRangeInfo
58 import androidx.compose.ui.semantics.clearAndSetSemantics
59 import androidx.compose.ui.semantics.contentDescription
60 import androidx.compose.ui.semantics.customActions
61 import androidx.compose.ui.semantics.disabled
62 import androidx.compose.ui.semantics.progressBarRangeInfo
63 import androidx.compose.ui.semantics.setProgress
64 import androidx.compose.ui.semantics.stateDescription
65 import androidx.compose.ui.unit.DpSize
66 import androidx.compose.ui.unit.dp
67 import com.android.compose.PlatformSlider
68 import com.android.compose.PlatformSliderColors
69 import com.android.systemui.Flags
70 import com.android.systemui.common.shared.model.Icon as IconModel
71 import com.android.systemui.common.ui.compose.Icon
72 import com.android.systemui.compose.modifiers.sysuiResTag
73 import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
74 import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
75 import com.android.systemui.lifecycle.rememberViewModel
76 import com.android.systemui.res.R
77 import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack
78 import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
79 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
80 import com.android.systemui.volume.ui.compose.slider.AccessibilityParams
81 import com.android.systemui.volume.ui.compose.slider.Haptics
82 import com.android.systemui.volume.ui.compose.slider.Slider
83 import com.android.systemui.volume.ui.compose.slider.SliderIcon
84 import kotlin.math.round
85 import kotlinx.coroutines.flow.distinctUntilChanged
86 import kotlinx.coroutines.flow.filter
87 import kotlinx.coroutines.flow.map
88 
89 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
90 @Composable
91 fun VolumeSlider(
92     state: SliderState,
93     onValueChange: (newValue: Float) -> Unit,
94     onIconTapped: () -> Unit,
95     sliderColors: PlatformSliderColors,
96     modifier: Modifier = Modifier,
97     hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
98     onValueChangeFinished: (() -> Unit)? = null,
99     button: (@Composable RowScope.() -> Unit)? = null,
100 ) {
101     if (!Flags.volumeRedesign()) {
102         LegacyVolumeSlider(
103             state = state,
104             onValueChange = onValueChange,
105             onIconTapped = onIconTapped,
106             sliderColors = sliderColors,
107             onValueChangeFinished = onValueChangeFinished,
108             modifier = modifier,
109             hapticsViewModelFactory = hapticsViewModelFactory,
110         )
111         return
112     }
113 
114     Column(modifier = modifier.animateContentSize()) {
115         Text(
116             text = state.label,
117             style = MaterialTheme.typography.titleMedium,
118             color = MaterialTheme.colorScheme.onSurface,
119             modifier = Modifier.fillMaxWidth().clearAndSetSemantics {},
120         )
121         Row(
122             horizontalArrangement = Arrangement.spacedBy(8.dp),
123             modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
124             verticalAlignment = Alignment.CenterVertically,
125         ) {
126             val materialSliderColors =
127                 SliderDefaults.colors(
128                     activeTickColor = MaterialTheme.colorScheme.surfaceContainerHigh,
129                     inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh,
130                     disabledActiveTickColor = MaterialTheme.colorScheme.surfaceContainerHigh,
131                     disabledInactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh,
132                 )
133             Slider(
134                 value = state.value,
135                 valueRange = state.valueRange,
136                 onValueChanged = onValueChange,
137                 onValueChangeFinished = { onValueChangeFinished?.invoke() },
138                 colors = materialSliderColors,
139                 isEnabled = state.isEnabled,
140                 stepDistance = state.step,
141                 accessibilityParams =
142                     AccessibilityParams(
143                         contentDescription = state.a11yContentDescription,
144                         stateDescription = state.a11yStateDescription,
145                     ),
146                 track = { sliderState ->
147                     SliderTrack(
148                         sliderState = sliderState,
149                         colors = materialSliderColors,
150                         isEnabled = state.isEnabled,
151                         activeTrackStartIcon =
152                             state.icon?.let { icon ->
153                                 { iconsState ->
154                                     SliderIcon(
155                                         icon = {
156                                             Icon(icon = icon, modifier = Modifier.size(24.dp))
157                                         },
158                                         isVisible = iconsState.isActiveTrackStartIconVisible,
159                                     )
160                                 }
161                             },
162                         inactiveTrackStartIcon =
163                             state.icon?.let { icon ->
164                                 { iconsState ->
165                                     SliderIcon(
166                                         icon = {
167                                             Icon(icon = icon, modifier = Modifier.size(24.dp))
168                                         },
169                                         isVisible = !iconsState.isActiveTrackStartIconVisible,
170                                     )
171                                 }
172                             },
173                     )
174                 },
175                 thumb = { sliderState, interactionSource ->
176                     SliderDefaults.Thumb(
177                         sliderState = sliderState,
178                         interactionSource = interactionSource,
179                         enabled = state.isEnabled,
180                         colors = materialSliderColors,
181                         thumbSize = DpSize(4.dp, 52.dp),
182                     )
183                 },
184                 haptics =
185                     hapticsViewModelFactory?.let {
186                         Haptics.Enabled(
187                             hapticsViewModelFactory = it,
188                             hapticFilter = state.hapticFilter,
189                             orientation = Orientation.Horizontal,
190                         )
191                     } ?: Haptics.Disabled,
192                 modifier = Modifier.weight(1f).sysuiResTag(state.label),
193             )
194             button?.invoke(this)
195         }
196         state.disabledMessage?.let { disabledMessage ->
197             AnimatedVisibility(visible = !state.isEnabled) {
198                 Row(
199                     modifier = Modifier.padding(bottom = 12.dp),
200                     horizontalArrangement = Arrangement.spacedBy(8.dp),
201                     verticalAlignment = Alignment.CenterVertically,
202                 ) {
203                     MaterialIcon(
204                         painter = painterResource(R.drawable.ic_error_outline),
205                         contentDescription = null,
206                         tint = MaterialTheme.colorScheme.onSurfaceVariant,
207                         modifier = Modifier.size(16.dp),
208                     )
209                     Text(
210                         text = disabledMessage,
211                         color = MaterialTheme.colorScheme.onSurfaceVariant,
212                         style = MaterialTheme.typography.labelSmall,
213                         modifier = Modifier.basicMarquee().clearAndSetSemantics {},
214                     )
215                 }
216             }
217         }
218     }
219 }
220 
221 @Composable
LegacyVolumeSlidernull222 private fun LegacyVolumeSlider(
223     state: SliderState,
224     onValueChange: (newValue: Float) -> Unit,
225     onIconTapped: () -> Unit,
226     sliderColors: PlatformSliderColors,
227     hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
228     modifier: Modifier = Modifier,
229     onValueChangeFinished: (() -> Unit)? = null,
230 ) {
231     val value by valueState(state)
232     val interactionSource = remember { MutableInteractionSource() }
233     val hapticsViewModel: SliderHapticsViewModel? =
234         setUpHapticsViewModel(
235             value,
236             state.valueRange,
237             state.hapticFilter,
238             interactionSource,
239             hapticsViewModelFactory,
240         )
241 
242     PlatformSlider(
243         modifier =
244             modifier.sysuiResTag(state.label).clearAndSetSemantics {
245                 if (state.isEnabled) {
246                     contentDescription = state.label
247                     state.a11yClickDescription?.let {
248                         customActions =
249                             listOf(
250                                 CustomAccessibilityAction(it) {
251                                     onIconTapped()
252                                     true
253                                 }
254                             )
255                     }
256 
257                     state.a11yStateDescription?.let { stateDescription = it }
258                     progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
259                 } else {
260                     disabled()
261                     contentDescription =
262                         state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
263                 }
264                 setProgress { targetValue ->
265                     val targetDirection =
266                         when {
267                             targetValue > value -> 1
268                             targetValue < value -> -1
269                             else -> 0
270                         }
271 
272                     val newValue =
273                         (value + targetDirection * state.step).coerceIn(
274                             state.valueRange.start,
275                             state.valueRange.endInclusive,
276                         )
277                     onValueChange(newValue)
278                     true
279                 }
280             },
281         value = value,
282         valueRange = state.valueRange,
283         onValueChange = { newValue ->
284             hapticsViewModel?.addVelocityDataPoint(newValue)
285             onValueChange(newValue)
286         },
287         onValueChangeFinished = {
288             hapticsViewModel?.onValueChangeEnded()
289             onValueChangeFinished?.invoke()
290         },
291         enabled = state.isEnabled,
292         icon = {
293             state.icon?.let {
294                 LegacySliderIcon(
295                     icon = it,
296                     onIconTapped = onIconTapped,
297                     isTappable = state.isMutable,
298                 )
299             }
300         },
301         colors = sliderColors,
302         label = { isDragging ->
303             AnimatedVisibility(
304                 visible = !isDragging,
305                 enter = fadeIn(tween(150)),
306                 exit = fadeOut(tween(150)),
307             ) {
308                 VolumeSliderContent(
309                     modifier = Modifier,
310                     label = state.label,
311                     isEnabled = state.isEnabled,
312                     disabledMessage = state.disabledMessage,
313                 )
314             }
315         },
316         interactionSource = interactionSource,
317     )
318 }
319 
320 @Composable
valueStatenull321 private fun valueState(state: SliderState): State<Float> {
322     var prevState by remember { mutableStateOf(state) }
323     // Don't animate slider value when receive the first value and when changing isEnabled state
324     val shouldSkipAnimation =
325         prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled
326     val value =
327         if (shouldSkipAnimation) remember { mutableFloatStateOf(state.value) }
328         else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
329     prevState = state
330     return value
331 }
332 
333 @Composable
LegacySliderIconnull334 private fun LegacySliderIcon(
335     icon: IconModel,
336     onIconTapped: () -> Unit,
337     isTappable: Boolean,
338     modifier: Modifier = Modifier,
339 ) {
340     val boxModifier =
341         if (isTappable) {
342                 modifier.clickable(
343                     onClick = onIconTapped,
344                     interactionSource = null,
345                     indication = null,
346                 )
347             } else {
348                 modifier
349             }
350             .fillMaxSize()
351     Box(
352         modifier = boxModifier,
353         contentAlignment = Alignment.Center,
354         content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
355     )
356 }
357 
358 @Composable
setUpHapticsViewModelnull359 private fun setUpHapticsViewModel(
360     value: Float,
361     valueRange: ClosedFloatingPointRange<Float>,
362     hapticFilter: SliderHapticFeedbackFilter,
363     interactionSource: MutableInteractionSource,
364     hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
365 ): SliderHapticsViewModel? {
366     return hapticsViewModelFactory?.let {
367         rememberViewModel(traceName = "SliderHapticsViewModel") {
368                 it.create(
369                     interactionSource,
370                     valueRange,
371                     Orientation.Horizontal,
372                     VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
373                         valueRange,
374                         hapticFilter,
375                     ),
376                     VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
377                 )
378             }
379             .also { hapticsViewModel ->
380                 var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) }
381                 LaunchedEffect(value) {
382                     snapshotFlow { value }
383                         .map { round(it) }
384                         .filter { it != lastDiscreteStep }
385                         .distinctUntilChanged()
386                         .collect { discreteStep ->
387                             lastDiscreteStep = discreteStep
388                             hapticsViewModel.onValueChange(discreteStep)
389                         }
390                 }
391             }
392     }
393 }
394