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.animateFloatAsState
21 import androidx.compose.animation.core.tween
22 import androidx.compose.animation.fadeIn
23 import androidx.compose.animation.fadeOut
24 import androidx.compose.foundation.clickable
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.size
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.State
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableFloatStateOf
32 import androidx.compose.runtime.mutableStateOf
33 import androidx.compose.runtime.remember
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.Alignment
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.semantics.CustomAccessibilityAction
38 import androidx.compose.ui.semantics.ProgressBarRangeInfo
39 import androidx.compose.ui.semantics.clearAndSetSemantics
40 import androidx.compose.ui.semantics.contentDescription
41 import androidx.compose.ui.semantics.customActions
42 import androidx.compose.ui.semantics.disabled
43 import androidx.compose.ui.semantics.progressBarRangeInfo
44 import androidx.compose.ui.semantics.setProgress
45 import androidx.compose.ui.semantics.stateDescription
46 import androidx.compose.ui.unit.dp
47 import com.android.compose.PlatformSlider
48 import com.android.compose.PlatformSliderColors
49 import com.android.systemui.common.shared.model.Icon
50 import com.android.systemui.common.ui.compose.Icon
51 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
52 
53 @Composable
54 fun VolumeSlider(
55     state: SliderState,
56     onValueChange: (newValue: Float) -> Unit,
57     onValueChangeFinished: (() -> Unit)? = null,
58     onIconTapped: () -> Unit,
59     modifier: Modifier = Modifier,
60     sliderColors: PlatformSliderColors,
61 ) {
62     val value by valueState(state)
63     PlatformSlider(
64         modifier =
65             modifier.clearAndSetSemantics {
66                 if (state.isEnabled) {
67                     contentDescription = state.label
68                     state.a11yClickDescription?.let {
69                         customActions =
70                             listOf(
71                                 CustomAccessibilityAction(it) {
72                                     onIconTapped()
73                                     true
74                                 }
75                             )
76                     }
77 
78                     state.a11yStateDescription?.let { stateDescription = it }
79                     progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
80                 } else {
81                     disabled()
82                     contentDescription =
83                         state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
84                 }
85                 setProgress { targetValue ->
86                     val targetDirection =
87                         when {
88                             targetValue > value -> 1
89                             targetValue < value -> -1
90                             else -> 0
91                         }
92 
93                     val newValue =
94                         (value + targetDirection * state.a11yStep).coerceIn(
95                             state.valueRange.start,
96                             state.valueRange.endInclusive
97                         )
98                     onValueChange(newValue)
99                     true
100                 }
101             },
102         value = value,
103         valueRange = state.valueRange,
104         onValueChange = onValueChange,
105         onValueChangeFinished = onValueChangeFinished,
106         enabled = state.isEnabled,
107         icon = {
108             state.icon?.let {
109                 SliderIcon(
110                     icon = it,
111                     onIconTapped = onIconTapped,
112                     isTappable = state.isMutable,
113                 )
114             }
115         },
116         colors = sliderColors,
117         label = { isDragging ->
118             AnimatedVisibility(
119                 visible = !isDragging,
120                 enter = fadeIn(tween(150)),
121                 exit = fadeOut(tween(150)),
122             ) {
123                 VolumeSliderContent(
124                     modifier = Modifier,
125                     label = state.label,
126                     isEnabled = state.isEnabled,
127                     disabledMessage = state.disabledMessage,
128                 )
129             }
130         }
131     )
132 }
133 
134 @Composable
valueStatenull135 private fun valueState(state: SliderState): State<Float> {
136     var prevState by remember { mutableStateOf(state) }
137     // Don't animate slider value when receive the first value and when changing isEnabled state
138     val shouldSkipAnimation =
139         prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled
140     val value =
141         if (shouldSkipAnimation) mutableFloatStateOf(state.value)
142         else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
143     prevState = state
144     return value
145 }
146 
147 @Composable
SliderIconnull148 private fun SliderIcon(
149     icon: Icon,
150     onIconTapped: () -> Unit,
151     isTappable: Boolean,
152     modifier: Modifier = Modifier
153 ) {
154     val boxModifier =
155         if (isTappable) {
156                 modifier.clickable(
157                     onClick = onIconTapped,
158                     interactionSource = null,
159                     indication = null
160                 )
161             } else {
162                 modifier
163             }
164             .fillMaxSize()
165     Box(
166         modifier = boxModifier,
167         contentAlignment = Alignment.Center,
168         content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
169     )
170 }
171