1 /*
<lambda>null2 * Copyright 2021 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.material3
18
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.Row
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.layout.paddingFromBaseline
25 import androidx.compose.foundation.layout.widthIn
26 import androidx.compose.material3.internal.Icons
27 import androidx.compose.material3.internal.Strings
28 import androidx.compose.material3.internal.getString
29 import androidx.compose.material3.tokens.SnackbarTokens
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.CompositionLocalProvider
32 import androidx.compose.ui.Alignment
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.graphics.Color
35 import androidx.compose.ui.graphics.Shape
36 import androidx.compose.ui.layout.AlignmentLine
37 import androidx.compose.ui.layout.FirstBaseline
38 import androidx.compose.ui.layout.LastBaseline
39 import androidx.compose.ui.layout.Layout
40 import androidx.compose.ui.layout.layoutId
41 import androidx.compose.ui.text.TextStyle
42 import androidx.compose.ui.unit.dp
43 import androidx.compose.ui.util.fastFirst
44 import androidx.compose.ui.util.fastFirstOrNull
45 import kotlin.math.max
46 import kotlin.math.min
47
48 /**
49 * [Material Design snackbar](https://m3.material.io/components/snackbar/overview)
50 *
51 * Snackbars provide brief messages about app processes at the bottom of the screen.
52 *
53 * 
55 *
56 * Snackbars inform users of a process that an app has performed or will perform. They appear
57 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and
58 * they don’t require user input to disappear.
59 *
60 * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
61 *
62 * Snackbars with an action should not timeout or self-dismiss until the user performs another
63 * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
64 * page is not considered an action.
65 *
66 * This component provides only the visuals of the Snackbar. If you need to show a Snackbar with
67 * defaults on the screen, use [SnackbarHostState.showSnackbar]:
68 *
69 * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
70 *
71 * If you want to customize appearance of the Snackbar, you can pass your own version as a child of
72 * the [SnackbarHost] to the [Scaffold]:
73 *
74 * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
75 *
76 * For a multiline sample following the Material recommended spec of a maximum of 2 lines, see:
77 *
78 * @sample androidx.compose.material3.samples.ScaffoldWithMultilineSnackbar
79 * @param modifier the [Modifier] to be applied to this snackbar
80 * @param action action / button component to add as an action to the snackbar. Consider using
81 * [ColorScheme.inversePrimary] as the color for the action, if you do not have a predefined color
82 * you wish to use instead.
83 * @param dismissAction action / button component to add as an additional close affordance action
84 * when a snackbar is non self-dismissive. Consider using [ColorScheme.inverseOnSurface] as the
85 * color for the action, if you do not have a predefined color you wish to use instead.
86 * @param actionOnNewLine whether or not action should be put on a separate line. Recommended for
87 * action with long action text.
88 * @param shape defines the shape of this snackbar's container
89 * @param containerColor the color used for the background of this snackbar. Use [Color.Transparent]
90 * to have no color.
91 * @param contentColor the preferred color for content inside this snackbar
92 * @param actionContentColor the preferred content color for the optional [action] inside this
93 * snackbar
94 * @param dismissActionContentColor the preferred content color for the optional [dismissAction]
95 * inside this snackbar
96 * @param content content to show information about a process that an app has performed or will
97 * perform
98 */
99 @Composable
100 fun Snackbar(
101 modifier: Modifier = Modifier,
102 action: @Composable (() -> Unit)? = null,
103 dismissAction: @Composable (() -> Unit)? = null,
104 actionOnNewLine: Boolean = false,
105 shape: Shape = SnackbarDefaults.shape,
106 containerColor: Color = SnackbarDefaults.color,
107 contentColor: Color = SnackbarDefaults.contentColor,
108 actionContentColor: Color = SnackbarDefaults.actionContentColor,
109 dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
110 content: @Composable () -> Unit
111 ) {
112 Surface(
113 modifier = modifier,
114 shape = shape,
115 color = containerColor,
116 contentColor = contentColor,
117 shadowElevation = SnackbarTokens.ContainerElevation
118 ) {
119 val textStyle = SnackbarTokens.SupportingTextFont.value
120 val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
121 CompositionLocalProvider(LocalTextStyle provides textStyle) {
122 when {
123 actionOnNewLine && action != null ->
124 NewLineButtonSnackbar(
125 text = content,
126 action = action,
127 dismissAction = dismissAction,
128 actionTextStyle = actionTextStyle,
129 actionContentColor = actionContentColor,
130 dismissActionContentColor = dismissActionContentColor,
131 )
132 else ->
133 OneRowSnackbar(
134 text = content,
135 action = action,
136 dismissAction = dismissAction,
137 actionTextStyle = actionTextStyle,
138 actionTextColor = actionContentColor,
139 dismissActionColor = dismissActionContentColor,
140 )
141 }
142 }
143 }
144 }
145
146 /**
147 * [Material Design snackbar](https://m3.material.io/components/snackbar/overview)
148 *
149 * Snackbars provide brief messages about app processes at the bottom of the screen.
150 *
151 * 
153 *
154 * Snackbars inform users of a process that an app has performed or will perform. They appear
155 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and
156 * they don’t require user input to disappear.
157 *
158 * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
159 *
160 * Snackbars with an action should not timeout or self-dismiss until the user performs another
161 * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
162 * page is not considered an action.
163 *
164 * This version of snackbar is designed to work with [SnackbarData] provided by the [SnackbarHost],
165 * which is usually used inside of the [Scaffold].
166 *
167 * This components provides only the visuals of the Snackbar. If you need to show a Snackbar with
168 * defaults on the screen, use [SnackbarHostState.showSnackbar]:
169 *
170 * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
171 *
172 * If you want to customize appearance of the Snackbar, you can pass your own version as a child of
173 * the [SnackbarHost] to the [Scaffold]:
174 *
175 * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
176 *
177 * When a [SnackbarData.visuals] sets the Snackbar's duration as [SnackbarDuration.Indefinite], it's
178 * recommended to display an additional close affordance action. See
179 * [SnackbarVisuals.withDismissAction]:
180 *
181 * @sample androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
182 * @param snackbarData data about the current snackbar showing via [SnackbarHostState]
183 * @param modifier the [Modifier] to be applied to this snackbar
184 * @param actionOnNewLine whether or not action should be put on a separate line. Recommended for
185 * action with long action text.
186 * @param shape defines the shape of this snackbar's container
187 * @param containerColor the color used for the background of this snackbar. Use [Color.Transparent]
188 * to have no color.
189 * @param contentColor the preferred color for content inside this snackbar
190 * @param actionColor the color of the snackbar's action
191 * @param actionContentColor the preferred content color for the optional action inside this
192 * snackbar. See [SnackbarVisuals.actionLabel].
193 * @param dismissActionContentColor the preferred content color for the optional dismiss action
194 * inside this snackbar. See [SnackbarVisuals.withDismissAction].
195 */
196 @Composable
Snackbarnull197 fun Snackbar(
198 snackbarData: SnackbarData,
199 modifier: Modifier = Modifier,
200 actionOnNewLine: Boolean = false,
201 shape: Shape = SnackbarDefaults.shape,
202 containerColor: Color = SnackbarDefaults.color,
203 contentColor: Color = SnackbarDefaults.contentColor,
204 actionColor: Color = SnackbarDefaults.actionColor,
205 actionContentColor: Color = SnackbarDefaults.actionContentColor,
206 dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
207 ) {
208 val actionLabel = snackbarData.visuals.actionLabel
209 val actionComposable: (@Composable () -> Unit)? =
210 if (actionLabel != null) {
211 @Composable {
212 TextButton(
213 colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
214 onClick = { snackbarData.performAction() },
215 content = { Text(actionLabel) }
216 )
217 }
218 } else {
219 null
220 }
221 val dismissActionComposable: (@Composable () -> Unit)? =
222 if (snackbarData.visuals.withDismissAction) {
223 @Composable {
224 IconButton(
225 onClick = { snackbarData.dismiss() },
226 content = {
227 Icon(
228 Icons.Filled.Close,
229 contentDescription = getString(Strings.SnackbarDismiss),
230 )
231 }
232 )
233 }
234 } else {
235 null
236 }
237 Snackbar(
238 modifier = modifier.padding(12.dp),
239 action = actionComposable,
240 dismissAction = dismissActionComposable,
241 actionOnNewLine = actionOnNewLine,
242 shape = shape,
243 containerColor = containerColor,
244 contentColor = contentColor,
245 actionContentColor = actionContentColor,
246 dismissActionContentColor = dismissActionContentColor,
247 content = { Text(snackbarData.visuals.message) }
248 )
249 }
250
251 @Composable
NewLineButtonSnackbarnull252 private fun NewLineButtonSnackbar(
253 text: @Composable () -> Unit,
254 action: @Composable () -> Unit,
255 dismissAction: @Composable (() -> Unit)?,
256 actionTextStyle: TextStyle,
257 actionContentColor: Color,
258 dismissActionContentColor: Color
259 ) {
260 Column(
261 modifier =
262 Modifier
263 // Fill max width, up to ContainerMaxWidth.
264 .widthIn(max = ContainerMaxWidth)
265 .fillMaxWidth()
266 .padding(start = HorizontalSpacing, bottom = SeparateButtonExtraY)
267 ) {
268 Box(
269 Modifier.paddingFromBaseline(HeightToFirstLine, LongButtonVerticalOffset)
270 .padding(end = HorizontalSpacingButtonSide)
271 ) {
272 text()
273 }
274
275 Box(
276 Modifier.align(Alignment.End)
277 .padding(end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp)
278 ) {
279 Row {
280 CompositionLocalProvider(
281 LocalContentColor provides actionContentColor,
282 LocalTextStyle provides actionTextStyle,
283 content = action
284 )
285 if (dismissAction != null) {
286 CompositionLocalProvider(
287 LocalContentColor provides dismissActionContentColor,
288 content = dismissAction
289 )
290 }
291 }
292 }
293 }
294 }
295
296 @Composable
OneRowSnackbarnull297 private fun OneRowSnackbar(
298 text: @Composable () -> Unit,
299 action: @Composable (() -> Unit)?,
300 dismissAction: @Composable (() -> Unit)?,
301 actionTextStyle: TextStyle,
302 actionTextColor: Color,
303 dismissActionColor: Color
304 ) {
305 val textTag = "text"
306 val actionTag = "action"
307 val dismissActionTag = "dismissAction"
308 Layout(
309 {
310 Box(Modifier.layoutId(textTag).padding(vertical = SnackbarVerticalPadding)) { text() }
311 if (action != null) {
312 Box(Modifier.layoutId(actionTag)) {
313 CompositionLocalProvider(
314 LocalContentColor provides actionTextColor,
315 LocalTextStyle provides actionTextStyle,
316 content = action
317 )
318 }
319 }
320 if (dismissAction != null) {
321 Box(Modifier.layoutId(dismissActionTag)) {
322 CompositionLocalProvider(
323 LocalContentColor provides dismissActionColor,
324 content = dismissAction
325 )
326 }
327 }
328 },
329 modifier =
330 Modifier.padding(
331 start = HorizontalSpacing,
332 end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp
333 )
334 ) { measurables, constraints ->
335 val containerWidth = min(constraints.maxWidth, ContainerMaxWidth.roundToPx())
336 val actionButtonPlaceable =
337 measurables.fastFirstOrNull { it.layoutId == actionTag }?.measure(constraints)
338 val dismissButtonPlaceable =
339 measurables.fastFirstOrNull { it.layoutId == dismissActionTag }?.measure(constraints)
340 val actionButtonWidth = actionButtonPlaceable?.width ?: 0
341 val actionButtonHeight = actionButtonPlaceable?.height ?: 0
342 val dismissButtonWidth = dismissButtonPlaceable?.width ?: 0
343 val dismissButtonHeight = dismissButtonPlaceable?.height ?: 0
344 val extraSpacingWidth = if (dismissButtonWidth == 0) TextEndExtraSpacing.roundToPx() else 0
345 val textMaxWidth =
346 (containerWidth - actionButtonWidth - dismissButtonWidth - extraSpacingWidth)
347 .coerceAtLeast(constraints.minWidth)
348 val textPlaceable =
349 measurables
350 .fastFirst { it.layoutId == textTag }
351 .measure(constraints.copy(minHeight = 0, maxWidth = textMaxWidth))
352
353 val firstTextBaseline = textPlaceable[FirstBaseline]
354 val lastTextBaseline = textPlaceable[LastBaseline]
355 val hasText =
356 firstTextBaseline != AlignmentLine.Unspecified &&
357 lastTextBaseline != AlignmentLine.Unspecified
358 val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
359 val dismissButtonPlaceX = containerWidth - dismissButtonWidth
360 val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth
361
362 val textPlaceY: Int
363 val containerHeight: Int
364 val actionButtonPlaceY: Int
365 if (isOneLine) {
366 val minContainerHeight = SnackbarTokens.SingleLineContainerHeight.roundToPx()
367 val contentHeight = max(actionButtonHeight, dismissButtonHeight)
368 containerHeight = max(minContainerHeight, contentHeight)
369 textPlaceY = (containerHeight - textPlaceable.height) / 2
370 actionButtonPlaceY =
371 if (actionButtonPlaceable != null) {
372 actionButtonPlaceable[FirstBaseline].let {
373 if (it != AlignmentLine.Unspecified) {
374 textPlaceY + firstTextBaseline - it
375 } else {
376 0
377 }
378 }
379 } else {
380 0
381 }
382 } else {
383 val baselineOffset = HeightToFirstLine.roundToPx()
384 textPlaceY = baselineOffset - firstTextBaseline
385 val minContainerHeight = SnackbarTokens.TwoLinesContainerHeight.roundToPx()
386 val contentHeight = textPlaceY + textPlaceable.height
387 containerHeight = max(minContainerHeight, contentHeight)
388 actionButtonPlaceY =
389 if (actionButtonPlaceable != null) {
390 (containerHeight - actionButtonPlaceable.height) / 2
391 } else {
392 0
393 }
394 }
395 val dismissButtonPlaceY =
396 if (dismissButtonPlaceable != null) {
397 (containerHeight - dismissButtonPlaceable.height) / 2
398 } else {
399 0
400 }
401
402 layout(containerWidth, containerHeight) {
403 textPlaceable.placeRelative(0, textPlaceY)
404 dismissButtonPlaceable?.placeRelative(dismissButtonPlaceX, dismissButtonPlaceY)
405 actionButtonPlaceable?.placeRelative(actionButtonPlaceX, actionButtonPlaceY)
406 }
407 }
408 }
409
410 /** Contains the default values used for [Snackbar]. */
411 object SnackbarDefaults {
412 /** Default shape of a snackbar. */
413 val shape: Shape
414 @Composable get() = SnackbarTokens.ContainerShape.value
415
416 /** Default color of a snackbar. */
417 val color: Color
418 @Composable get() = SnackbarTokens.ContainerColor.value
419
420 /** Default content color of a snackbar. */
421 val contentColor: Color
422 @Composable get() = SnackbarTokens.SupportingTextColor.value
423
424 /** Default action color of a snackbar. */
425 val actionColor: Color
426 @Composable get() = SnackbarTokens.ActionLabelTextColor.value
427
428 /** Default action content color of a snackbar. */
429 val actionContentColor: Color
430 @Composable get() = SnackbarTokens.ActionLabelTextColor.value
431
432 /** Default dismiss action content color of a snackbar. */
433 val dismissActionContentColor: Color
434 @Composable get() = SnackbarTokens.IconColor.value
435 }
436
437 private val ContainerMaxWidth = 600.dp
438 private val HeightToFirstLine = 30.dp
439 private val HorizontalSpacing = 16.dp
440 private val HorizontalSpacingButtonSide = 8.dp
441 private val SeparateButtonExtraY = 2.dp
442 private val SnackbarVerticalPadding = 6.dp
443 private val TextEndExtraSpacing = 8.dp
444 private val LongButtonVerticalOffset = 12.dp
445