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