1 /*
<lambda>null2 * 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.systemui.dialog.ui.composable
18
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.PaddingValues
22 import androidx.compose.foundation.layout.Spacer
23 import androidx.compose.foundation.layout.defaultMinSize
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.height
26 import androidx.compose.foundation.layout.padding
27 import androidx.compose.foundation.rememberScrollState
28 import androidx.compose.foundation.verticalScroll
29 import androidx.compose.material3.LocalContentColor
30 import androidx.compose.material3.MaterialTheme
31 import androidx.compose.material3.ProvideTextStyle
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.CompositionLocalProvider
34 import androidx.compose.ui.Alignment
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.layout.Layout
37 import androidx.compose.ui.layout.Placeable
38 import androidx.compose.ui.layout.layoutId
39 import androidx.compose.ui.text.style.TextAlign
40 import androidx.compose.ui.unit.dp
41 import kotlin.math.roundToInt
42
43 /**
44 * The content of an AlertDialog which can be used together with
45 * [SystemUIDialogFactory.create][com.android.systemui.statusbar.phone.create] to create an alert
46 * dialog in Compose.
47 *
48 * @see com.android.systemui.statusbar.phone.create
49 */
50 @Composable
51 fun AlertDialogContent(
52 title: @Composable () -> Unit,
53 content: @Composable () -> Unit,
54 modifier: Modifier = Modifier,
55 icon: (@Composable () -> Unit)? = null,
56 positiveButton: (@Composable () -> Unit)? = null,
57 negativeButton: (@Composable () -> Unit)? = null,
58 neutralButton: (@Composable () -> Unit)? = null,
59 ) {
60 Column(
61 modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(DialogPaddings),
62 horizontalAlignment = Alignment.CenterHorizontally,
63 ) {
64 // Icon.
65 if (icon != null) {
66 val defaultSize = 32.dp
67 Box(
68 Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
69 propagateMinConstraints = true,
70 ) {
71 val iconColor = MaterialTheme.colorScheme.primary
72 CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
73 }
74
75 Spacer(Modifier.height(16.dp))
76 }
77
78 // Title.
79 val titleColor = MaterialTheme.colorScheme.onSurface
80 CompositionLocalProvider(LocalContentColor provides titleColor) {
81 ProvideTextStyle(
82 MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
83 ) {
84 title()
85 }
86 }
87 Spacer(Modifier.height(16.dp))
88
89 // Content.
90 val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
91 Box {
92 CompositionLocalProvider(LocalContentColor provides contentColor) {
93 ProvideTextStyle(
94 MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center)
95 ) {
96 content()
97 }
98 }
99 }
100 Spacer(Modifier.height(32.dp))
101
102 // Buttons.
103 if (positiveButton != null || negativeButton != null || neutralButton != null) {
104 AlertDialogButtons(
105 positiveButton = positiveButton,
106 negativeButton = negativeButton,
107 neutralButton = neutralButton,
108 )
109 }
110 }
111 }
112
113 @Composable
AlertDialogButtonsnull114 private fun AlertDialogButtons(
115 positiveButton: (@Composable () -> Unit)?,
116 negativeButton: (@Composable () -> Unit)?,
117 neutralButton: (@Composable () -> Unit)?,
118 modifier: Modifier = Modifier,
119 ) {
120 Layout(
121 content = {
122 positiveButton?.let { Box(Modifier.layoutId("positive")) { it() } }
123 negativeButton?.let { Box(Modifier.layoutId("negative")) { it() } }
124 neutralButton?.let { Box(Modifier.layoutId("neutral")) { it() } }
125 },
126 modifier,
127 ) { measurables, constraints ->
128 check(constraints.hasBoundedWidth) {
129 "AlertDialogButtons should not be composed in an horizontally scrollable layout"
130 }
131 val maxWidth = constraints.maxWidth
132
133 // Measure the buttons.
134 var positive: Placeable? = null
135 var negative: Placeable? = null
136 var neutral: Placeable? = null
137 for (i in measurables.indices) {
138 val measurable = measurables[i]
139 when (val layoutId = measurable.layoutId) {
140 "positive" -> positive = measurable.measure(constraints)
141 "negative" -> negative = measurable.measure(constraints)
142 "neutral" -> neutral = measurable.measure(constraints)
143 else -> error("Unexpected layoutId=$layoutId")
144 }
145 }
146
147 fun Placeable?.width() = this?.width ?: 0
148 fun Placeable?.height() = this?.height ?: 0
149
150 // The min horizontal spacing between buttons.
151 val horizontalSpacing = 8.dp.toPx()
152 val totalHorizontalSpacing = (measurables.size - 1) * horizontalSpacing
153 val requiredWidth =
154 positive.width() + negative.width() + neutral.width() + totalHorizontalSpacing
155
156 if (requiredWidth <= maxWidth) {
157 // Stack horizontally: [neutral][flexSpace][negative][positive].
158 val height = maxOf(positive.height(), negative.height(), neutral.height())
159 layout(maxWidth, height) {
160 positive?.let { it.placeRelative(maxWidth - it.width, 0) }
161
162 negative?.let { negative ->
163 if (positive == null) {
164 negative.placeRelative(maxWidth - negative.width, 0)
165 } else {
166 negative.placeRelative(
167 maxWidth -
168 negative.width -
169 positive.width -
170 horizontalSpacing.roundToInt(),
171 0,
172 )
173 }
174 }
175
176 neutral?.placeRelative(0, 0)
177 }
178 } else {
179 // Stack vertically, aligned on the right (in LTR layouts):
180 // [positive]
181 // [negative]
182 // [neutral]
183 //
184 // TODO(b/283817398): Introduce a ResponsiveDialogButtons composable to create buttons
185 // that have different styles when stacked horizontally, as shown in
186 // go/sysui-dialog-styling.
187 val height = positive.height() + negative.height() + neutral.height()
188 layout(maxWidth, height) {
189 var y = 0
190 fun Placeable.place() {
191 placeRelative(maxWidth - width, y)
192 y += this.height
193 }
194
195 positive?.place()
196 negative?.place()
197 neutral?.place()
198 }
199 }
200 }
201 }
202
203 private val DialogPaddings = PaddingValues(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 18.dp)
204