• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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