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