• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.mechanics.demo.tuneable
18 
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.animateColorAsState
21 import androidx.compose.animation.core.animateFloatAsState
22 import androidx.compose.animation.fadeIn
23 import androidx.compose.animation.fadeOut
24 import androidx.compose.foundation.BorderStroke
25 import androidx.compose.foundation.clickable
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.ColumnScope
29 import androidx.compose.foundation.layout.Row
30 import androidx.compose.foundation.layout.Spacer
31 import androidx.compose.foundation.layout.fillMaxWidth
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.layout.width
34 import androidx.compose.material.icons.Icons
35 import androidx.compose.material.icons.outlined.ExpandLess
36 import androidx.compose.material3.Icon
37 import androidx.compose.material3.MaterialTheme
38 import androidx.compose.material3.Surface
39 import androidx.compose.material3.Text
40 import androidx.compose.runtime.Composable
41 import androidx.compose.runtime.CompositionLocalProvider
42 import androidx.compose.runtime.MutableState
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.remember
45 import androidx.compose.runtime.staticCompositionLocalOf
46 import androidx.compose.ui.Alignment
47 import androidx.compose.ui.Modifier
48 import androidx.compose.ui.draw.rotate
49 import androidx.compose.ui.graphics.Color
50 import androidx.compose.ui.text.style.TextOverflow
51 import androidx.compose.ui.unit.dp
52 
53 typealias ConfigurationContent<T> =
54     @Composable ColumnScope.(value: T, onValueChanged: (T) -> Unit) -> Unit
55 
56 @Composable
SectionDescriptionnull57 fun SectionDescription(shortDescription: String, modifier: Modifier = Modifier) {
58     Column(modifier = modifier.fillMaxWidth()) {
59         Text(shortDescription, style = MaterialTheme.typography.bodySmall)
60     }
61 }
62 
63 data class SectionData(
64     val keyPrefix: String,
65     val expansionStateFactory: (String) -> MutableState<Boolean>,
66 )
67 
<lambda>null68 val LocalSectionData = staticCompositionLocalOf<SectionData> { throw AssertionError() }
69 
70 @Composable
Sectionnull71 fun <T> Section(
72     label: String,
73     summary: (T) -> String,
74     value: T,
75     onValueChanged: (T) -> Unit,
76     sectionKey: String,
77     modifier: Modifier = Modifier,
78     showSummaryWhenExpanded: Boolean = false,
79     subsections: ConfigurationContent<T>? = null,
80     content: ConfigurationContent<T>,
81 ) {
82     val sectionData = LocalSectionData.current
83     val isExpanded = sectionData.expansionStateFactory(sectionKey)
84 
85     val borderColor by
86         animateColorAsState(
87             if (isExpanded.value) MaterialTheme.colorScheme.outlineVariant else Color.Transparent,
88             label = "Border",
89         )
90 
91     Surface(
92         shape = MaterialTheme.shapes.medium,
93         color = MaterialTheme.colorScheme.surface,
94         tonalElevation = 1.dp,
95         border = BorderStroke(1.dp, borderColor),
96         modifier = modifier,
97     ) {
98         Column(modifier = Modifier.fillMaxWidth()) {
99             Row(
100                 verticalAlignment = Alignment.CenterVertically,
101                 modifier =
102                     Modifier.fillMaxWidth().clickable { isExpanded.value = !isExpanded.value },
103             ) {
104                 val iconRotation by
105                     animateFloatAsState(
106                         if (isExpanded.value) 0f else 180f,
107                         label = "Expansion icon rotation",
108                     )
109                 Icon(
110                     Icons.Outlined.ExpandLess,
111                     contentDescription = "Expand / collapse section",
112                     modifier = Modifier.padding(4.dp).rotate(iconRotation),
113                 )
114 
115                 Text(label, style = MaterialTheme.typography.titleMedium)
116                 Spacer(Modifier.width(8.dp))
117                 AnimatedVisibility(
118                     showSummaryWhenExpanded || !isExpanded.value,
119                     enter = fadeIn(),
120                     exit = fadeOut(),
121                 ) {
122                     Text(
123                         summary(value),
124                         overflow = TextOverflow.Ellipsis,
125                         style = MaterialTheme.typography.titleSmall,
126                         color = MaterialTheme.colorScheme.onSurfaceVariant,
127                         maxLines = 1,
128                         modifier = Modifier.fillMaxWidth(),
129                     )
130                 }
131             }
132 
133             AnimatedVisibility(isExpanded.value) {
134                 Column {
135                     Column(
136                         verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top),
137                         // Use an alignment line instead of a padding
138                         modifier =
139                             Modifier.padding(start = 32.dp, top = 4.dp, end = 8.dp, bottom = 8.dp),
140                     ) {
141                         content(value, onValueChanged)
142                     }
143 
144                     if (subsections != null) {
145                         val subSectionData =
146                             remember(sectionData, sectionKey) {
147                                 SectionData(
148                                     keyPrefix = "${sectionData.keyPrefix}-$sectionKey",
149                                     sectionData.expansionStateFactory,
150                                 )
151                             }
152                         CompositionLocalProvider(LocalSectionData provides subSectionData) {
153                             Column(
154                                 verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top),
155                                 modifier = Modifier.fillMaxWidth(),
156                             ) {
157                                 subsections(value, onValueChanged)
158                             }
159                         }
160                     }
161                 }
162             }
163         }
164     }
165 }
166