1 /*
<lambda>null2 * Copyright (C) 2022 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.layout.Arrangement
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.ColumnScope
22 import androidx.compose.foundation.layout.PaddingValues
23 import androidx.compose.foundation.layout.fillMaxSize
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.lazy.LazyColumn
27 import androidx.compose.foundation.lazy.LazyListState
28 import androidx.compose.foundation.lazy.rememberLazyListState
29 import androidx.compose.material.icons.Icons
30 import androidx.compose.material.icons.outlined.TouchApp
31 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
32 import androidx.compose.material3.MaterialTheme
33 import androidx.compose.material3.Text
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.CompositionLocalProvider
36 import androidx.compose.runtime.compositionLocalOf
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.runtime.setValue
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.draw.clip
43 import androidx.compose.ui.layout.onGloballyPositioned
44 import androidx.compose.ui.tooling.preview.Preview
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.dp
47 import com.android.settingslib.spa.framework.theme.SettingsDimension
48 import com.android.settingslib.spa.framework.theme.SettingsShape
49 import com.android.settingslib.spa.framework.theme.SettingsTheme
50 import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
51 import com.android.settingslib.spa.widget.preference.Preference
52 import com.android.settingslib.spa.widget.preference.PreferenceModel
53
54 /** A category title that is placed before a group of similar items. */
55 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
56 @Composable
57 fun CategoryTitle(title: String) {
58 Text(
59 text = title,
60 modifier =
61 Modifier.padding(
62 start =
63 if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall
64 else SettingsDimension.itemPaddingStart,
65 top = 20.dp,
66 end =
67 if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall
68 else SettingsDimension.itemPaddingEnd,
69 bottom = 8.dp,
70 ),
71 color = MaterialTheme.colorScheme.primary,
72 style =
73 if (isSpaExpressiveEnabled) MaterialTheme.typography.labelLargeEmphasized
74 else MaterialTheme.typography.labelMedium,
75 )
76 }
77
78 /**
79 * A container that is used to group similar items. A [Category] displays a [CategoryTitle] and
80 * visually separates groups of items.
81 *
82 * @param content The content of the category.
83 */
84 @Composable
Categorynull85 fun Category(
86 title: String? = null,
87 modifier: Modifier = Modifier,
88 content: @Composable ColumnScope.() -> Unit,
89 ) {
90 var displayTitle by remember { mutableStateOf(false) }
91 Column(
92 modifier =
93 if (isSpaExpressiveEnabled && displayTitle)
94 Modifier.padding(
95 horizontal = SettingsDimension.paddingLarge,
96 vertical = SettingsDimension.paddingSmall,
97 )
98 else Modifier
99 ) {
100 if (title != null && displayTitle) CategoryTitle(title = title)
101 Column(
102 modifier =
103 modifier
104 .onGloballyPositioned { coordinates ->
105 displayTitle = coordinates.size.height > 0
106 }
107 .then(
108 if (isSpaExpressiveEnabled)
109 Modifier.fillMaxWidth().clip(SettingsShape.CornerMedium2)
110 else Modifier
111 ),
112 verticalArrangement =
113 if (isSpaExpressiveEnabled) Arrangement.spacedBy(SettingsDimension.paddingTiny)
114 else Arrangement.Top,
115 content = { CompositionLocalProvider(LocalIsInCategory provides true) { content() } },
116 )
117 }
118 }
119
120 /**
121 * A container that is used to group items with lazy loading.
122 *
123 * @param list The list of items to display.
124 * @param entry The entry for each list item according to its index in list.
125 * @param key Optional. The key for each item in list to provide unique item identifiers, making the
126 * list more efficient.
127 * @param title Optional. Category title for each item or each group of items in the list. It should
128 * be decided by the index.
129 * @param bottomPadding Optional. Bottom outside padding of the category.
130 * @param state Optional. State of LazyList.
131 * @param footer Optional. Content to be shown at the bottom of the category.
132 * @param header Optional. Content to be shown at the top of the category.
133 */
134 @Composable
LazyCategorynull135 fun LazyCategory(
136 list: List<Any>,
137 entry: (Int) -> @Composable () -> Unit,
138 key: ((Int) -> Any)? = null,
139 title: ((Int) -> String?)? = null,
140 bottomPadding: Dp = SettingsDimension.paddingSmall,
141 state: LazyListState = rememberLazyListState(),
142 footer: @Composable () -> Unit = {},
143 header: @Composable () -> Unit,
144 ) {
145 Column(
146 Modifier.padding(
147 PaddingValues(
148 start = SettingsDimension.paddingLarge,
149 end = SettingsDimension.paddingLarge,
150 top = SettingsDimension.paddingSmall,
151 bottom = bottomPadding,
152 )
153 )
154 .clip(SettingsShape.CornerMedium2)
<lambda>null155 ) {
156 LazyColumn(
157 modifier = Modifier.fillMaxSize(),
158 verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
159 state = state,
160 ) {
161 item { CompositionLocalProvider(LocalIsInCategory provides true) { header() } }
162
163 items(count = list.size, key = key) {
164 title?.invoke(it)?.let { title -> CategoryTitle(title) }
165 CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() }
166 }
167
168 item { CompositionLocalProvider(LocalIsInCategory provides true) { footer() } }
169 }
170 }
171 }
172
173 /** LocalIsInCategory containing the if the current composable is in a category. */
<lambda>null174 internal val LocalIsInCategory = compositionLocalOf { false }
175
176 @Preview
177 @Composable
CategoryPreviewnull178 private fun CategoryPreview() {
179 SettingsTheme {
180 Category(title = "Appearance") {
181 Preference(
182 object : PreferenceModel {
183 override val title = "Title"
184 override val summary = { "Summary" }
185 }
186 )
187 Preference(
188 object : PreferenceModel {
189 override val title = "Title"
190 override val summary = { "Summary" }
191 override val icon =
192 @Composable { SettingsIcon(imageVector = Icons.Outlined.TouchApp) }
193 }
194 )
195 }
196 }
197 }
198
199 @Preview
200 @Composable
LazyCategoryPreviewnull201 private fun LazyCategoryPreview() {
202 SettingsTheme {
203 LazyCategory(
204 list = listOf(1, 2, 3),
205 entry = { key ->
206 @Composable {
207 Preference(
208 object : PreferenceModel {
209 override val title = key.toString()
210 }
211 )
212 }
213 },
214 footer = @Composable {
215 Footer("Footer")
216 },
217 header = @Composable {
218 Text("Header")
219 },
220 )
221 }
222 }
223