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  * ![Snackbar
54  * image](https://developer.android.com/images/reference/androidx/compose/material3/snackbar.png)
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  * ![Snackbar
152  * image](https://developer.android.com/images/reference/androidx/compose/material3/snackbar.png)
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