1 /*
2 * Copyright (C) 2023 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.settingslib.spa.widget.ui
18
19 import androidx.compose.foundation.background
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.PaddingValues
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.Spacer
24 import androidx.compose.foundation.layout.heightIn
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.layout.size
27 import androidx.compose.foundation.selection.selectableGroup
28 import androidx.compose.material.icons.Icons
29 import androidx.compose.material.icons.outlined.Check
30 import androidx.compose.material.icons.outlined.ExpandLess
31 import androidx.compose.material.icons.outlined.ExpandMore
32 import androidx.compose.material3.Button
33 import androidx.compose.material3.ButtonDefaults
34 import androidx.compose.material3.DropdownMenu
35 import androidx.compose.material3.DropdownMenuItem
36 import androidx.compose.material3.Icon
37 import androidx.compose.material3.MaterialTheme
38 import androidx.compose.material3.Text
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableIntStateOf
42 import androidx.compose.runtime.mutableStateOf
43 import androidx.compose.runtime.saveable.rememberSaveable
44 import androidx.compose.runtime.setValue
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.semantics.Role
49 import androidx.compose.ui.semantics.role
50 import androidx.compose.ui.semantics.semantics
51 import androidx.compose.ui.tooling.preview.Preview
52 import androidx.compose.ui.unit.dp
53 import com.android.settingslib.spa.framework.theme.SettingsDimension
54 import com.android.settingslib.spa.framework.theme.SettingsShape
55 import com.android.settingslib.spa.framework.theme.SettingsTheme
56 import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
57
58 data class SpinnerOption(val id: Int, val text: String)
59
60 @Composable
Spinnernull61 fun Spinner(options: List<SpinnerOption>, selectedId: Int?, setId: (id: Int) -> Unit) {
62 if (options.isEmpty()) {
63 return
64 }
65
66 var expanded by rememberSaveable { mutableStateOf(false) }
67
68 Box(
69 modifier =
70 Modifier.padding(
71 start = SettingsDimension.itemPaddingStart,
72 top = SettingsDimension.itemPaddingAround,
73 end = SettingsDimension.itemPaddingEnd,
74 bottom = SettingsDimension.itemPaddingAround,
75 )
76 .selectableGroup()
77 ) {
78 if (isSpaExpressiveEnabled) {
79 Button(
80 modifier = Modifier.semantics { role = Role.DropdownList },
81 onClick = { expanded = true },
82 colors =
83 ButtonDefaults.buttonColors(
84 containerColor = MaterialTheme.colorScheme.secondaryContainer,
85 contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
86 ),
87 contentPadding =
88 PaddingValues(
89 horizontal = SettingsDimension.spinnerHorizontalPadding,
90 vertical = SettingsDimension.spinnerVerticalPadding,
91 ),
92 ) {
93 SpinnerText(options.find { it.id == selectedId })
94 ExpandIcon(expanded)
95 }
96 DropdownMenu(
97 expanded = expanded,
98 onDismissRequest = { expanded = false },
99 shape = SettingsShape.CornerLarge,
100 modifier =
101 Modifier.background(MaterialTheme.colorScheme.surfaceContainerLow)
102 .padding(horizontal = SettingsDimension.paddingSmall),
103 ) {
104 for (option in options) {
105 val selected = option.id == selectedId
106 DropdownMenuItem(
107 text = { SpinnerOptionText(option = option, selected) },
108 onClick = {
109 expanded = false
110 setId(option.id)
111 },
112 contentPadding =
113 PaddingValues(
114 horizontal = SettingsDimension.paddingSmall,
115 vertical = SettingsDimension.paddingExtraSmall1,
116 ),
117 modifier =
118 Modifier.heightIn(min = SettingsDimension.spinnerOptionMinHeight)
119 .then(
120 if (selected)
121 Modifier.clip(SettingsShape.CornerMedium2)
122 .background(MaterialTheme.colorScheme.primaryContainer)
123 else Modifier
124 ),
125 )
126 }
127 }
128 } else {
129 val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd)
130 Button(
131 modifier = Modifier.semantics { role = Role.DropdownList },
132 onClick = { expanded = true },
133 colors =
134 ButtonDefaults.buttonColors(
135 containerColor = MaterialTheme.colorScheme.primaryContainer,
136 contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
137 ),
138 contentPadding = contentPadding,
139 ) {
140 SpinnerText(options.find { it.id == selectedId })
141 ExpandIcon(expanded)
142 }
143 DropdownMenu(
144 expanded = expanded,
145 onDismissRequest = { expanded = false },
146 modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer),
147 ) {
148 for (option in options) {
149 DropdownMenuItem(
150 text = {
151 SpinnerText(
152 option = option,
153 modifier = Modifier.padding(end = 24.dp),
154 color = MaterialTheme.colorScheme.onSecondaryContainer,
155 )
156 },
157 onClick = {
158 expanded = false
159 setId(option.id)
160 },
161 contentPadding = contentPadding,
162 )
163 }
164 }
165 }
166 }
167 }
168
169 @Composable
ExpandIconnull170 internal fun ExpandIcon(expanded: Boolean) {
171 Icon(
172 imageVector =
173 when {
174 expanded -> Icons.Outlined.ExpandLess
175 else -> Icons.Outlined.ExpandMore
176 },
177 contentDescription = null,
178 )
179 }
180
181 @Composable
SpinnerTextnull182 private fun SpinnerText(
183 option: SpinnerOption?,
184 modifier: Modifier = Modifier,
185 color: Color = Color.Unspecified,
186 ) {
187 Text(
188 text = option?.text ?: "",
189 modifier =
190 modifier
191 .padding(end = SettingsDimension.itemPaddingEnd)
192 .then(
193 if (!isSpaExpressiveEnabled)
194 Modifier.padding(vertical = SettingsDimension.itemPaddingAround)
195 else Modifier
196 ),
197 color = color,
198 style =
199 if (isSpaExpressiveEnabled) MaterialTheme.typography.titleMedium
200 else MaterialTheme.typography.labelLarge,
201 )
202 }
203
204 @Composable
SpinnerOptionTextnull205 private fun SpinnerOptionText(option: SpinnerOption?, selected: Boolean) {
206 Row {
207 if (selected) {
208 Icon(
209 imageVector = Icons.Outlined.Check,
210 modifier = Modifier.size(SettingsDimension.spinnerIconSize),
211 tint = MaterialTheme.colorScheme.onPrimaryContainer,
212 contentDescription = null,
213 )
214 Spacer(Modifier.padding(SettingsDimension.paddingSmall))
215 }
216 Text(
217 text = option?.text ?: "",
218 modifier = Modifier.padding(end = SettingsDimension.itemPaddingEnd),
219 color =
220 if (selected) MaterialTheme.colorScheme.onPrimaryContainer
221 else MaterialTheme.colorScheme.onSurface,
222 style = MaterialTheme.typography.labelLarge,
223 )
224 }
225 }
226
227 @Preview(showBackground = true)
228 @Composable
SpinnerPreviewnull229 private fun SpinnerPreview() {
230 SettingsTheme {
231 var selectedId by rememberSaveable { mutableIntStateOf(1) }
232 Spinner(
233 options = (1..3).map { SpinnerOption(id = it, text = "Option $it") },
234 selectedId = selectedId,
235 setId = { selectedId = it },
236 )
237 }
238 }
239