[go: nahoru, domu]

blob: baac4479f3e8107b118e8cffa3c69cf78e725cb9 [file] [log] [blame]
Matvei Malkovb814df02019-11-21 15:58:03 +00001/*
2 * Copyright 2019 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
17package androidx.ui.material
18
19import androidx.compose.Composable
Matvei Malkovb814df02019-11-21 15:58:03 +000020import androidx.ui.core.CurrentTextStyleProvider
21import androidx.ui.core.FirstBaseline
Matvei Malkovb814df02019-11-21 15:58:03 +000022import androidx.ui.core.LastBaseline
23import androidx.ui.core.Layout
Mihai Popa8c474e92019-12-10 17:43:20 +000024import androidx.ui.core.LayoutTagParentData
Matvei Malkovb814df02019-11-21 15:58:03 +000025import androidx.ui.core.Modifier
Mihai Popa8c474e92019-12-10 17:43:20 +000026import androidx.ui.core.ParentData
Matvei Malkovb814df02019-11-21 15:58:03 +000027import androidx.ui.core.Text
Mihai Popa8c474e92019-12-10 17:43:20 +000028import androidx.ui.core.tag
Matvei Malkov59bac362020-02-13 20:23:13 +000029import androidx.ui.foundation.DrawBackground
Matvei Malkovb814df02019-11-21 15:58:03 +000030import androidx.ui.foundation.shape.corner.RoundedCornerShape
31import androidx.ui.graphics.Color
32import androidx.ui.layout.AlignmentLineOffset
33import androidx.ui.layout.Column
34import androidx.ui.layout.Container
Adam Powell712dc992019-12-04 12:48:30 -080035import androidx.ui.layout.LayoutGravity
36import androidx.ui.layout.LayoutPadding
Adam Powell31c1ebd2020-01-09 09:48:24 -080037import androidx.ui.layout.LayoutWidth
Matvei Malkovb814df02019-11-21 15:58:03 +000038import androidx.ui.material.surface.Surface
George Mount842c8c12020-01-08 16:03:42 -080039import androidx.ui.unit.IntPx
40import androidx.ui.unit.dp
41import androidx.ui.unit.ipx
42import androidx.ui.unit.max
Matvei Malkovb814df02019-11-21 15:58:03 +000043
44/**
45 * Snackbars provide brief messages about app processes at the bottom of the screen.
46 *
47 * Snackbars inform users of a process that an app has performed or will perform. They appear
48 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
49 * and they don’t require user input to disappear.
50 *
51 * A snackbar can contain a single action. Because they disappear automatically, the action
52 * shouldn’t be “Dismiss” or “Cancel.”
53 *
54 * @sample androidx.ui.material.samples.SimpleSnackbar
55 *
56 * @param text information about a process that an app has performed or will perform
57 * @param actionText action name in the snackbar. If null, there will be text label with no
58 * action button
59 * @param onActionClick lambda to be invoked when the action is clicked
60 * @param modifier modifiers for the the Snackbar layout
61 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
62 * for action with long action text
63 */
64@Composable
65fun Snackbar(
66 text: String,
67 actionText: String? = null,
68 onActionClick: (() -> Unit)? = null,
69 modifier: Modifier = Modifier.None,
70 actionOnNewLine: Boolean = false
71) {
72 val actionSlot: @Composable() (() -> Unit)? =
73 if (actionText != null) {
74 @Composable {
Louis Pullen-Freiliche386f97dd2020-01-31 17:32:33 +000075 TextButton(
Matvei Malkovb814df02019-11-21 15:58:03 +000076 onClick = onActionClick,
Louis Pullen-Freiliche386f97dd2020-01-31 17:32:33 +000077 // TODO: remove this when primary light variant is figured out
78 contentColor = makePrimaryVariantLight(MaterialTheme.colors().primary)
79 ) {
80 Text(actionText)
81 }
Matvei Malkovb814df02019-11-21 15:58:03 +000082 }
83 } else {
84 null
85 }
86 Snackbar(
87 modifier = modifier,
88 actionOnNewLine = actionOnNewLine,
89 text = { Text(text, maxLines = TextMaxLines) },
90 action = actionSlot
91 )
92}
93
94/**
95 * Snackbars provide brief messages about app processes at the bottom of the screen.
96 *
97 * Snackbars inform users of a process that an app has performed or will perform. They appear
98 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
99 * and they don’t require user input to disappear.
100 *
101 * A snackbar can contain a single action. Because they disappear automatically, the action
102 * shouldn’t be “Dismiss” or “Cancel.”
103 *
104 * This version provides more granular control over the content of the Snackbar. Use it if you
105 * want to customize the content inside.
106 *
107 * @sample androidx.ui.material.samples.SlotsSnackbar
108 *
109 * @param text text component to show information about a process that an app has performed or
110 * will perform
111 * @param action action / button component to add as an action to the snackbar
112 * @param modifier modifiers for the the Snackbar layout
113 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
114 * for action with long action text
115 */
116@Composable
117fun Snackbar(
118 text: @Composable() () -> Unit,
119 action: @Composable() (() -> Unit)? = null,
120 modifier: Modifier = Modifier.None,
121 actionOnNewLine: Boolean = false
122) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800123 val colors = MaterialTheme.colors()
Matvei Malkovb814df02019-11-21 15:58:03 +0000124 Surface(
125 modifier = modifier,
126 shape = SnackbarShape,
127 elevation = SnackbarElevation,
128 color = colors.surface
129 ) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800130 val textStyle = MaterialTheme.typography().body2.copy(color = colors.surface)
Matvei Malkov59bac362020-02-13 20:23:13 +0000131 val additionalBackground = DrawBackground(
132 color = colors.onSurface.copy(alpha = SnackbarOverlayAlpha),
133 shape = SnackbarShape
Matvei Malkovb814df02019-11-21 15:58:03 +0000134 )
135 CurrentTextStyleProvider(value = textStyle) {
136 when {
Matvei Malkov59bac362020-02-13 20:23:13 +0000137 action == null -> TextOnlySnackbar(additionalBackground, text)
138 actionOnNewLine -> NewLineButtonSnackbar(additionalBackground, text, action)
139 else -> OneRowSnackbar(additionalBackground, text, action)
Matvei Malkovb814df02019-11-21 15:58:03 +0000140 }
141 }
142 }
143}
144
145@Composable
Matvei Malkov59bac362020-02-13 20:23:13 +0000146private fun TextOnlySnackbar(modifier: Modifier = Modifier.None, text: @Composable() () -> Unit) {
Matvei Malkovb814df02019-11-21 15:58:03 +0000147 Layout(
Matvei Malkov59bac362020-02-13 20:23:13 +0000148 text,
149 modifier = modifier + LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacing)
Matvei Malkovb814df02019-11-21 15:58:03 +0000150 ) { measurables, constraints ->
151 require(measurables.size == 1) {
152 "text for Snackbar expected to have exactly only one child"
153 }
154 val textPlaceable = measurables.first().measure(constraints)
155 val firstBaseline = requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
156 val lastBaseline = requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
157
158 val minHeight = if (firstBaseline == lastBaseline) MinHeightOneLine else MinHeightTwoLines
159 layout(constraints.maxWidth, max(minHeight.toIntPx(), textPlaceable.height)) {
160 val textPlaceY = HeightToFirstLine.toIntPx() - firstBaseline
161 textPlaceable.place(0.ipx, textPlaceY)
162 }
163 }
164}
165
166@Composable
167private fun NewLineButtonSnackbar(
Matvei Malkov59bac362020-02-13 20:23:13 +0000168 modifier: Modifier = Modifier.None,
Matvei Malkovb814df02019-11-21 15:58:03 +0000169 text: @Composable() () -> Unit,
170 button: @Composable() () -> Unit
171) {
172 Column(
Matvei Malkov59bac362020-02-13 20:23:13 +0000173 modifier = modifier + LayoutWidth.Fill + LayoutPadding(
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000174 start = HorizontalSpacing,
175 end = HorizontalSpacingButtonSide,
Matvei Malkovb814df02019-11-21 15:58:03 +0000176 bottom = SeparateButtonExtraY
177 )
178 ) {
179 AlignmentLineOffset(alignmentLine = LastBaseline, after = LongButtonVerticalOffset) {
180 AlignmentLineOffset(alignmentLine = FirstBaseline, before = HeightToFirstLine) {
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000181 Container(LayoutPadding(end = HorizontalSpacingButtonSide), children = text)
Matvei Malkovb814df02019-11-21 15:58:03 +0000182 }
183 }
Adam Powell712dc992019-12-04 12:48:30 -0800184 Container(modifier = LayoutGravity.End, children = button)
Matvei Malkovb814df02019-11-21 15:58:03 +0000185 }
186}
187
188@Composable
189private fun OneRowSnackbar(
Matvei Malkov59bac362020-02-13 20:23:13 +0000190 modifier: Modifier = Modifier.None,
Matvei Malkovb814df02019-11-21 15:58:03 +0000191 text: @Composable() () -> Unit,
192 button: @Composable() () -> Unit
193) {
194 Layout(
Mihai Popa8c474e92019-12-10 17:43:20 +0000195 {
196 ParentData(
197 object : LayoutTagParentData {
198 override val tag: Any = "text"
199 },
200 text
201 )
202 ParentData(
203 object : LayoutTagParentData {
204 override val tag: Any = "button"
205 },
206 button
207 )
208 },
Matvei Malkov59bac362020-02-13 20:23:13 +0000209 modifier = modifier +
210 LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacingButtonSide)
Matvei Malkovb814df02019-11-21 15:58:03 +0000211 ) { measurables, constraints ->
Mihai Popa8c474e92019-12-10 17:43:20 +0000212 val buttonPlaceable = measurables.first { it.tag == "button" }.measure(constraints)
Matvei Malkovb814df02019-11-21 15:58:03 +0000213 val textMaxWidth =
214 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
215 .coerceAtLeast(constraints.minWidth)
Mihai Popa8c474e92019-12-10 17:43:20 +0000216 val textPlaceable = measurables.first { it.tag == "text" }.measure(
217 constraints.copy(minHeight = IntPx.Zero, maxWidth = textMaxWidth)
218 )
Matvei Malkovb814df02019-11-21 15:58:03 +0000219
220 val firstTextBaseline =
221 requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
222 val lastTextBaseline =
223 requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
224 val baselineOffset = HeightToFirstLine.toIntPx()
225 val isOneLine = firstTextBaseline == lastTextBaseline
226 val textPlaceY = baselineOffset - firstTextBaseline
227 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
228
229 val containerHeight: IntPx
230 val buttonPlaceY: IntPx
231 if (isOneLine) {
232 val minContainerHeight = MinHeightOneLine.toIntPx()
233 val contentHeight = buttonPlaceable.height + SingleTextYPadding.toIntPx() * 2
234 containerHeight = max(minContainerHeight, contentHeight)
235 val buttonBaseline = buttonPlaceable.get(FirstBaseline)
236 buttonPlaceY =
237 buttonBaseline?.let { baselineOffset - it } ?: SingleTextYPadding.toIntPx()
238 } else {
239 val minContainerHeight = MinHeightTwoLines.toIntPx()
240 val contentHeight = textPlaceY + textPlaceable.height
241 containerHeight = max(minContainerHeight, contentHeight)
242 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
243 }
244
245 layout(constraints.maxWidth, containerHeight) {
246 textPlaceable.place(0.ipx, textPlaceY)
247 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
248 }
249 }
250}
251
252// TODO: remove this when primary light variant is figured out in MaterialTheme
253private fun makePrimaryVariantLight(primary: Color): Color {
254 val blendColor = Color.White.copy(alpha = 0.6f)
255 return Color(
256 red = blendColor.red * blendColor.alpha + primary.red * (1 - blendColor.alpha),
257 green = blendColor.green * blendColor.alpha + primary.green * (1 - blendColor.alpha),
258 blue = blendColor.blue * blendColor.alpha + primary.blue * (1 - blendColor.alpha)
259 )
260}
261
262private val TextMaxLines = 2
263private val SnackbarOverlayAlpha = 0.8f
264private val SnackbarShape = RoundedCornerShape(4.dp)
265private val SnackbarElevation = 6.dp
266
267private val MinHeightOneLine = 48.dp
268private val MinHeightTwoLines = 68.dp
269private val HeightToFirstLine = 30.dp
270private val HorizontalSpacing = 16.dp
271private val HorizontalSpacingButtonSide = 8.dp
272private val SeparateButtonExtraY = 8.dp
273private val SingleTextYPadding = 6.dp
274private val TextEndExtraSpacing = 8.dp
275private val LongButtonVerticalOffset = 18.dp