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  * ![Snackbars
56  * image](https://developer.android.com/images/reference/androidx/compose/material/snackbars.png)
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  * ![Snackbars
128  * image](https://developer.android.com/images/reference/androidx/compose/material/snackbars.png)
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