1 /*
<lambda>null2 * 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
17 package androidx.compose.material
18
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.fillMaxWidth
22 import androidx.compose.foundation.layout.padding
23 import androidx.compose.foundation.layout.paddingFromBaseline
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.CompositionLocalProvider
26 import androidx.compose.ui.Alignment
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.graphics.Color
29 import androidx.compose.ui.graphics.Shape
30 import androidx.compose.ui.graphics.compositeOver
31 import androidx.compose.ui.layout.AlignmentLine
32 import androidx.compose.ui.layout.FirstBaseline
33 import androidx.compose.ui.layout.LastBaseline
34 import androidx.compose.ui.layout.Layout
35 import androidx.compose.ui.layout.Placeable
36 import androidx.compose.ui.layout.layoutId
37 import androidx.compose.ui.unit.Dp
38 import androidx.compose.ui.unit.dp
39 import androidx.compose.ui.util.fastFirst
40 import androidx.compose.ui.util.fastForEach
41 import kotlin.math.max
42
43 /**
44 * [Material Design snackbar](https://material.io/components/snackbars)
45 *
46 * Snackbars provide brief messages about app processes at the bottom of the screen.
47 *
48 * Snackbars inform users of a process that an app has performed or will perform. They appear
49 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and
50 * they don’t require user input to disappear.
51 *
52 * A Snackbar can contain a single action. Because Snackbar disappears automatically, the action
53 * shouldn't be "Dismiss" or "Cancel".
54 *
55 * 
57 *
58 * This components provides only the visuals of the [Snackbar]. If you need to show a [Snackbar]
59 * with defaults on the screen, use [ScaffoldState.snackbarHostState] and
60 * [SnackbarHostState.showSnackbar]:
61 *
62 * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
63 *
64 * If you want to customize appearance of the [Snackbar], you can pass your own version as a child
65 * of the [SnackbarHost] to the [Scaffold]:
66 *
67 * @sample androidx.compose.material.samples.ScaffoldWithCustomSnackbar
68 * @param modifier modifiers for the Snackbar layout
69 * @param action action / button component to add as an action to the snackbar. Consider using
70 * [SnackbarDefaults.primaryActionColor] as the color for the action, if you do not have a
71 * predefined color you wish to use instead.
72 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended for
73 * action with long action text
74 * @param shape Defines the Snackbar's shape as well as its shadow
75 * @param backgroundColor background color of the Snackbar
76 * @param contentColor color of the content to use inside the snackbar. Defaults to either the
77 * matching content color for [backgroundColor], or, if it is not a color from the theme, this
78 * will keep the same value set above this Surface.
79 * @param elevation The z-coordinate at which to place the SnackBar. This controls the size of the
80 * shadow below the SnackBar
81 * @param content content to show information about a process that an app has performed or will
82 * perform
83 */
84 @Composable
85 fun Snackbar(
86 modifier: Modifier = Modifier,
87 action: @Composable (() -> Unit)? = null,
88 actionOnNewLine: Boolean = false,
89 shape: Shape = MaterialTheme.shapes.small,
90 backgroundColor: Color = SnackbarDefaults.backgroundColor,
91 contentColor: Color = MaterialTheme.colors.surface,
92 elevation: Dp = 6.dp,
93 content: @Composable () -> Unit
94 ) {
95 Surface(
96 modifier = modifier,
97 shape = shape,
98 elevation = elevation,
99 color = backgroundColor,
100 contentColor = contentColor
101 ) {
102 CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
103 val textStyle = MaterialTheme.typography.body2
104 ProvideTextStyle(value = textStyle) {
105 when {
106 action == null -> TextOnlySnackbar(content)
107 actionOnNewLine -> NewLineButtonSnackbar(content, action)
108 else -> OneRowSnackbar(content, action)
109 }
110 }
111 }
112 }
113 }
114
115 /**
116 * [Material Design snackbar](https://material.io/components/snackbars)
117 *
118 * Snackbars provide brief messages about app processes at the bottom of the screen.
119 *
120 * Snackbars inform users of a process that an app has performed or will perform. They appear
121 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and
122 * they don’t require user input to disappear.
123 *
124 * A Snackbar can contain a single action. Because they disappear automatically, the action
125 * shouldn't be "Dismiss" or "Cancel".
126 *
127 * 
129 *
130 * This version of snackbar is designed to work with [SnackbarData] provided by the [SnackbarHost],
131 * which is usually used inside of the [Scaffold].
132 *
133 * This components provides only the visuals of the [Snackbar]. If you need to show a [Snackbar]
134 * with defaults on the screen, use [ScaffoldState.snackbarHostState] and
135 * [SnackbarHostState.showSnackbar]:
136 *
137 * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
138 *
139 * If you want to customize appearance of the [Snackbar], you can pass your own version as a child
140 * of the [SnackbarHost] to the [Scaffold]:
141 *
142 * @sample androidx.compose.material.samples.ScaffoldWithCustomSnackbar
143 * @param snackbarData data about the current snackbar showing via [SnackbarHostState]
144 * @param modifier modifiers for the Snackbar layout
145 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended for
146 * action with long action text
147 * @param shape Defines the Snackbar's shape as well as its shadow
148 * @param backgroundColor background color of the Snackbar
149 * @param contentColor color of the content to use inside the snackbar. Defaults to either the
150 * matching content color for [backgroundColor], or, if it is not a color from the theme, this
151 * will keep the same value set above this Surface.
152 * @param actionColor color of the action
153 * @param elevation The z-coordinate at which to place the SnackBar. This controls the size of the
154 * shadow below the SnackBar
155 */
156 @Composable
Snackbarnull157 fun Snackbar(
158 snackbarData: SnackbarData,
159 modifier: Modifier = Modifier,
160 actionOnNewLine: Boolean = false,
161 shape: Shape = MaterialTheme.shapes.small,
162 backgroundColor: Color = SnackbarDefaults.backgroundColor,
163 contentColor: Color = MaterialTheme.colors.surface,
164 actionColor: Color = SnackbarDefaults.primaryActionColor,
165 elevation: Dp = 6.dp
166 ) {
167 val actionLabel = snackbarData.actionLabel
168 val actionComposable: (@Composable () -> Unit)? =
169 if (actionLabel != null) {
170 @Composable {
171 TextButton(
172 colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
173 onClick = { snackbarData.performAction() },
174 content = { Text(actionLabel) }
175 )
176 }
177 } else {
178 null
179 }
180 Snackbar(
181 modifier = modifier.padding(12.dp),
182 content = { Text(snackbarData.message) },
183 action = actionComposable,
184 actionOnNewLine = actionOnNewLine,
185 shape = shape,
186 backgroundColor = backgroundColor,
187 contentColor = contentColor,
188 elevation = elevation
189 )
190 }
191
192 /** Object to hold defaults used by [Snackbar] */
193 object SnackbarDefaults {
194
195 /** Default alpha of the overlay applied to the [backgroundColor] */
196 private const val SnackbarOverlayAlpha = 0.8f
197
198 /** Default background color of the [Snackbar] */
199 val backgroundColor: Color
200 @Composable
201 get() =
202 MaterialTheme.colors.onSurface
203 .copy(alpha = SnackbarOverlayAlpha)
204 .compositeOver(MaterialTheme.colors.surface)
205
206 /**
207 * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
208 * Given that [Snackbar]s have an 'inverted' theme, i.e. in a light theme they appear dark, and
209 * in a dark theme they appear light, just using [Colors.primary] will not work, and has
210 * incorrect contrast.
211 *
212 * If your light theme has a corresponding dark theme, you should instead directly use
213 * [Colors.primary] from the dark theme when in a light theme, and use [Colors.primaryVariant]
214 * from the dark theme when in a dark theme.
215 *
216 * When in a light theme, this function applies a color overlay to [Colors.primary] from
217 * [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
218 * function uses [Colors.primaryVariant].
219 */
220 val primaryActionColor: Color
221 @Composable
222 get() {
223 val colors = MaterialTheme.colors
224 return if (colors.isLight) {
225 val primary = colors.primary
226 val overlayColor = colors.surface.copy(alpha = 0.6f)
227
228 overlayColor.compositeOver(primary)
229 } else {
230 colors.primaryVariant
231 }
232 }
233 }
234
235 @Composable
TextOnlySnackbarnull236 private fun TextOnlySnackbar(content: @Composable () -> Unit) {
237 Layout({
238 Box(
239 modifier =
240 Modifier.padding(horizontal = HorizontalSpacing, vertical = SnackbarVerticalPadding)
241 ) {
242 content()
243 }
244 }) { measurables, constraints ->
245 val textPlaceables = ArrayList<Placeable>(measurables.size)
246 var firstBaseline = AlignmentLine.Unspecified
247 var lastBaseline = AlignmentLine.Unspecified
248 var height = 0
249
250 measurables.fastForEach {
251 val placeable = it.measure(constraints)
252 textPlaceables.add(placeable)
253 if (
254 placeable[FirstBaseline] != AlignmentLine.Unspecified &&
255 (firstBaseline == AlignmentLine.Unspecified ||
256 placeable[FirstBaseline] < firstBaseline)
257 ) {
258 firstBaseline = placeable[FirstBaseline]
259 }
260 if (
261 placeable[LastBaseline] != AlignmentLine.Unspecified &&
262 (lastBaseline == AlignmentLine.Unspecified ||
263 placeable[LastBaseline] > lastBaseline)
264 ) {
265 lastBaseline = placeable[LastBaseline]
266 }
267 height = max(height, placeable.height)
268 }
269
270 val hasText =
271 firstBaseline != AlignmentLine.Unspecified && lastBaseline != AlignmentLine.Unspecified
272
273 val minHeight =
274 if (firstBaseline == lastBaseline || !hasText) {
275 SnackbarMinHeightOneLine
276 } else {
277 SnackbarMinHeightTwoLines
278 }
279 val containerHeight = max(minHeight.roundToPx(), height)
280 layout(constraints.maxWidth, containerHeight) {
281 textPlaceables.fastForEach {
282 val textPlaceY = (containerHeight - it.height) / 2
283 it.placeRelative(0, textPlaceY)
284 }
285 }
286 }
287 }
288
289 @Composable
NewLineButtonSnackbarnull290 private fun NewLineButtonSnackbar(text: @Composable () -> Unit, action: @Composable () -> Unit) {
291 Column(
292 modifier =
293 Modifier.fillMaxWidth()
294 .padding(
295 start = HorizontalSpacing,
296 end = HorizontalSpacingButtonSide,
297 bottom = SeparateButtonExtraY
298 )
299 ) {
300 Box(
301 Modifier.paddingFromBaseline(HeightToFirstLine, LongButtonVerticalOffset)
302 .padding(end = HorizontalSpacingButtonSide)
303 ) {
304 text()
305 }
306 Box(Modifier.align(Alignment.End)) { action() }
307 }
308 }
309
310 @Composable
OneRowSnackbarnull311 private fun OneRowSnackbar(text: @Composable () -> Unit, action: @Composable () -> Unit) {
312 val textTag = "text"
313 val actionTag = "action"
314 Layout(
315 {
316 Box(Modifier.layoutId(textTag).padding(vertical = SnackbarVerticalPadding)) { text() }
317 Box(Modifier.layoutId(actionTag)) { action() }
318 },
319 modifier = Modifier.padding(start = HorizontalSpacing, end = HorizontalSpacingButtonSide)
320 ) { measurables, constraints ->
321 val buttonPlaceable =
322 measurables.fastFirst { it.layoutId == actionTag }.measure(constraints)
323 val textMaxWidth =
324 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.roundToPx())
325 .coerceAtLeast(constraints.minWidth)
326 val textPlaceable =
327 measurables
328 .fastFirst { it.layoutId == textTag }
329 .measure(constraints.copy(minHeight = 0, maxWidth = textMaxWidth))
330
331 val firstTextBaseline = textPlaceable[FirstBaseline]
332 val lastTextBaseline = textPlaceable[LastBaseline]
333 val hasText =
334 firstTextBaseline != AlignmentLine.Unspecified &&
335 lastTextBaseline != AlignmentLine.Unspecified
336 val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
337 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
338
339 val textPlaceY: Int
340 val containerHeight: Int
341 val buttonPlaceY: Int
342 if (isOneLine) {
343 val minContainerHeight = SnackbarMinHeightOneLine.roundToPx()
344 val contentHeight = buttonPlaceable.height
345 containerHeight = max(minContainerHeight, contentHeight)
346 textPlaceY = (containerHeight - textPlaceable.height) / 2
347 val buttonBaseline = buttonPlaceable[FirstBaseline]
348 buttonPlaceY =
349 buttonBaseline.let {
350 if (it != AlignmentLine.Unspecified) {
351 textPlaceY + firstTextBaseline - it
352 } else {
353 0
354 }
355 }
356 } else {
357 val baselineOffset = HeightToFirstLine.roundToPx()
358 textPlaceY = baselineOffset - firstTextBaseline
359 val minContainerHeight = SnackbarMinHeightTwoLines.roundToPx()
360 val contentHeight = textPlaceY + textPlaceable.height
361 containerHeight = max(minContainerHeight, contentHeight)
362 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
363 }
364
365 layout(constraints.maxWidth, containerHeight) {
366 textPlaceable.placeRelative(0, textPlaceY)
367 buttonPlaceable.placeRelative(buttonPlaceX, buttonPlaceY)
368 }
369 }
370 }
371
372 private val HeightToFirstLine = 30.dp
373 private val HorizontalSpacing = 16.dp
374 private val HorizontalSpacingButtonSide = 8.dp
375 private val SeparateButtonExtraY = 2.dp
376 private val SnackbarVerticalPadding = 6.dp
377 private val TextEndExtraSpacing = 8.dp
378 private val LongButtonVerticalOffset = 12.dp
379 private val SnackbarMinHeightOneLine = 48.dp
380 private val SnackbarMinHeightTwoLines = 68.dp
381