[go: nahoru, domu]

blob: 68a8e0ef1ed1155752d9ee89d5a753e3e51a8ccc [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
Mihai Popa5b7a6bb2020-04-08 20:11:03 +010020import androidx.ui.core.Alignment
Mihai Popa08c47952020-06-04 20:01:27 +010021import androidx.ui.core.AlignmentLine
Matvei Malkovb814df02019-11-21 15:58:03 +000022import androidx.ui.core.Layout
23import androidx.ui.core.Modifier
Mihai Popa4d1d8142020-06-08 16:08:16 +010024import androidx.ui.core.id
25import androidx.ui.core.layoutId
Louis Pullen-Freilichddda7be2020-07-17 18:28:12 +010026import androidx.compose.foundation.Box
27import androidx.compose.foundation.ProvideTextStyle
Louis Pullen-Freilich4dc4dac2020-07-22 14:39:14 +010028import androidx.compose.ui.graphics.Color
29import androidx.compose.ui.graphics.Shape
30import androidx.compose.ui.graphics.compositeOver
Louis Pullen-Freilich623e4052020-07-19 20:24:03 +010031import androidx.compose.foundation.layout.Column
32import androidx.compose.foundation.layout.fillMaxWidth
33import androidx.compose.foundation.layout.padding
34import androidx.compose.foundation.layout.relativePaddingFrom
Louis Pullen-Freilichca6eca22020-07-20 00:31:45 +010035import androidx.compose.foundation.text.FirstBaseline
36import androidx.compose.foundation.text.LastBaseline
Andrey Kulikov6e28e972020-03-25 12:31:24 +000037import androidx.ui.unit.Dp
George Mount842c8c12020-01-08 16:03:42 -080038import androidx.ui.unit.dp
George Mount8f237572020-04-30 12:08:30 -070039import kotlin.math.max
Matvei Malkovb814df02019-11-21 15:58:03 +000040
41/**
42 * Snackbars provide brief messages about app processes at the bottom of the screen.
43 *
44 * Snackbars inform users of a process that an app has performed or will perform. They appear
45 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
46 * and they don’t require user input to disappear.
47 *
Louis Pullen-Freilich644c22e2020-03-02 16:05:01 +000048 * A Snackbar can contain a single action. Because they disappear automatically, the action
49 * shouldn't be "Dismiss" or "Cancel".
Matvei Malkovb814df02019-11-21 15:58:03 +000050 *
51 * @sample androidx.ui.material.samples.SimpleSnackbar
52 *
Matvei Malkovb814df02019-11-21 15:58:03 +000053 * @param text text component to show information about a process that an app has performed or
54 * will perform
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000055 * @param action action / button component to add as an action to the snackbar. Consider using
56 * [snackbarPrimaryColorFor] as the color for the action, if you do not have a predefined color
57 * you wish to use instead.
Matvei Malkovb814df02019-11-21 15:58:03 +000058 * @param modifier modifiers for the the Snackbar layout
59 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
60 * for action with long action text
Andrey Kulikov6e28e972020-03-25 12:31:24 +000061 * @param shape Defines the Snackbar's shape as well as its shadow
62 * @param elevation The z-coordinate at which to place the SnackBar. This controls the size
63 * of the shadow below the SnackBar
Matvei Malkovb814df02019-11-21 15:58:03 +000064 */
65@Composable
66fun Snackbar(
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +010067 text: @Composable () -> Unit,
68 action: @Composable (() -> Unit)? = null,
Adam Powellb6d8db22020-04-02 12:40:03 -070069 modifier: Modifier = Modifier,
Andrey Kulikov6e28e972020-03-25 12:31:24 +000070 actionOnNewLine: Boolean = false,
71 shape: Shape = MaterialTheme.shapes.small,
72 elevation: Dp = 6.dp
Matvei Malkovb814df02019-11-21 15:58:03 +000073) {
Louis Pullen-Freilichb2591852020-03-23 19:00:09 +000074 val colors = MaterialTheme.colors
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000075 // Snackbar has a background color of onSurface with an alpha applied blended on top of surface
76 val snackbarOverlayColor = colors.onSurface.copy(alpha = SnackbarOverlayAlpha)
77 val snackbarColor = snackbarOverlayColor.compositeOver(colors.surface)
Matvei Malkovb814df02019-11-21 15:58:03 +000078 Surface(
79 modifier = modifier,
Andrey Kulikov6e28e972020-03-25 12:31:24 +000080 shape = shape,
81 elevation = elevation,
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000082 color = snackbarColor,
83 contentColor = colors.surface
Matvei Malkovb814df02019-11-21 15:58:03 +000084 ) {
Louis Pullen-Freilich98318632020-03-27 00:32:05 +000085 ProvideEmphasis(EmphasisAmbient.current.high) {
Louis Pullen-Freilichb2591852020-03-23 19:00:09 +000086 val textStyle = MaterialTheme.typography.body2
Louis Pullen-Freilich0d044382020-03-17 22:43:35 +000087 ProvideTextStyle(value = textStyle) {
Louis Pullen-Freilichd3c21532020-03-02 19:23:23 +000088 when {
89 action == null -> TextOnlySnackbar(text)
90 actionOnNewLine -> NewLineButtonSnackbar(text, action)
91 else -> OneRowSnackbar(text, action)
92 }
Matvei Malkovb814df02019-11-21 15:58:03 +000093 }
94 }
95 }
96}
97
98@Composable
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +010099private fun TextOnlySnackbar(text: @Composable () -> Unit) {
Matvei Malkovb814df02019-11-21 15:58:03 +0000100 Layout(
Matvei Malkov59bac362020-02-13 20:23:13 +0000101 text,
Matvei Malkov99f3d292020-06-26 13:24:45 +0100102 modifier = Modifier.padding(
103 start = HorizontalSpacing,
104 end = HorizontalSpacing,
105 top = SnackbarVerticalPadding,
106 bottom = SnackbarVerticalPadding
107 )
Anastasia Soboleva5e382dd2020-06-17 21:51:38 +0100108 ) { measurables, constraints ->
Matvei Malkovb814df02019-11-21 15:58:03 +0000109 require(measurables.size == 1) {
110 "text for Snackbar expected to have exactly only one child"
111 }
112 val textPlaceable = measurables.first().measure(constraints)
Mihai Popa08c47952020-06-04 20:01:27 +0100113 val firstBaseline = textPlaceable[FirstBaseline]
114 val lastBaseline = textPlaceable[LastBaseline]
115 require(firstBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
116 require(lastBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
Matvei Malkovb814df02019-11-21 15:58:03 +0000117
Matvei Malkov99f3d292020-06-26 13:24:45 +0100118 val minHeight =
119 if (firstBaseline == lastBaseline) {
120 SnackbarMinHeightOneLine
121 } else {
122 SnackbarMinHeightTwoLines
123 }
124 val containerHeight = max(minHeight.toIntPx(), textPlaceable.height)
125 layout(constraints.maxWidth, containerHeight) {
126 val textPlaceY = (containerHeight - textPlaceable.height) / 2
George Mount8f237572020-04-30 12:08:30 -0700127 textPlaceable.place(0, textPlaceY)
Matvei Malkovb814df02019-11-21 15:58:03 +0000128 }
129 }
130}
131
132@Composable
133private fun NewLineButtonSnackbar(
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +0100134 text: @Composable () -> Unit,
135 action: @Composable () -> Unit
Matvei Malkovb814df02019-11-21 15:58:03 +0000136) {
137 Column(
Adam Powell999a89b2020-03-11 09:08:07 -0700138 modifier = Modifier.fillMaxWidth()
139 .padding(
140 start = HorizontalSpacing,
141 end = HorizontalSpacingButtonSide,
142 bottom = SeparateButtonExtraY
143 )
Matvei Malkovb814df02019-11-21 15:58:03 +0000144 ) {
Anastasia Soboleva57c818f2020-05-06 00:13:51 +0100145 Box(
146 Modifier
147 .relativePaddingFrom(LastBaseline, after = LongButtonVerticalOffset)
148 .relativePaddingFrom(FirstBaseline, before = HeightToFirstLine)
149 .padding(end = HorizontalSpacingButtonSide),
150 children = text
151 )
Mihai Popa5b7a6bb2020-04-08 20:11:03 +0100152 Box(Modifier.gravity(Alignment.End), children = action)
Matvei Malkovb814df02019-11-21 15:58:03 +0000153 }
154}
155
156@Composable
157private fun OneRowSnackbar(
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +0100158 text: @Composable () -> Unit,
159 action: @Composable () -> Unit
Matvei Malkovb814df02019-11-21 15:58:03 +0000160) {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000161 val textTag = "text"
162 val actionTag = "action"
Matvei Malkovb814df02019-11-21 15:58:03 +0000163 Layout(
Mihai Popa8c474e92019-12-10 17:43:20 +0000164 {
Mihai Popa4d1d8142020-06-08 16:08:16 +0100165 Box(Modifier.layoutId(textTag), children = text)
166 Box(Modifier.layoutId(actionTag), children = action)
Mihai Popa8c474e92019-12-10 17:43:20 +0000167 },
Matvei Malkov99f3d292020-06-26 13:24:45 +0100168 modifier = Modifier.padding(
169 start = HorizontalSpacing,
170 end = HorizontalSpacingButtonSide,
171 top = SnackbarVerticalPadding,
172 bottom = SnackbarVerticalPadding
173 )
Anastasia Soboleva5e382dd2020-06-17 21:51:38 +0100174 ) { measurables, constraints ->
Mihai Popa4d1d8142020-06-08 16:08:16 +0100175 val buttonPlaceable = measurables.first { it.id == actionTag }.measure(constraints)
Matvei Malkovb814df02019-11-21 15:58:03 +0000176 val textMaxWidth =
177 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
178 .coerceAtLeast(constraints.minWidth)
Mihai Popa4d1d8142020-06-08 16:08:16 +0100179 val textPlaceable = measurables.first { it.id == textTag }.measure(
George Mount8f237572020-04-30 12:08:30 -0700180 constraints.copy(minHeight = 0, maxWidth = textMaxWidth)
Mihai Popa8c474e92019-12-10 17:43:20 +0000181 )
Matvei Malkovb814df02019-11-21 15:58:03 +0000182
Mihai Popa08c47952020-06-04 20:01:27 +0100183 val firstTextBaseline = textPlaceable[FirstBaseline]
184 require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
185 val lastTextBaseline = textPlaceable[LastBaseline]
186 require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
Matvei Malkovb814df02019-11-21 15:58:03 +0000187 val isOneLine = firstTextBaseline == lastTextBaseline
Matvei Malkovb814df02019-11-21 15:58:03 +0000188 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
189
Matvei Malkov99f3d292020-06-26 13:24:45 +0100190 val textPlaceY: Int
George Mount8f237572020-04-30 12:08:30 -0700191 val containerHeight: Int
192 val buttonPlaceY: Int
Matvei Malkovb814df02019-11-21 15:58:03 +0000193 if (isOneLine) {
Matvei Malkov99f3d292020-06-26 13:24:45 +0100194 val minContainerHeight = SnackbarMinHeightOneLine.toIntPx()
195 val contentHeight = buttonPlaceable.height
Matvei Malkovb814df02019-11-21 15:58:03 +0000196 containerHeight = max(minContainerHeight, contentHeight)
Matvei Malkov99f3d292020-06-26 13:24:45 +0100197 textPlaceY = (containerHeight - textPlaceable.height) / 2
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000198 val buttonBaseline = buttonPlaceable[FirstBaseline]
Mihai Popa08c47952020-06-04 20:01:27 +0100199 buttonPlaceY = buttonBaseline.let {
200 if (it != AlignmentLine.Unspecified) {
Matvei Malkov99f3d292020-06-26 13:24:45 +0100201 textPlaceY + firstTextBaseline - it
Mihai Popa08c47952020-06-04 20:01:27 +0100202 } else {
Matvei Malkov99f3d292020-06-26 13:24:45 +0100203 0
Mihai Popa08c47952020-06-04 20:01:27 +0100204 }
205 }
Matvei Malkovb814df02019-11-21 15:58:03 +0000206 } else {
Matvei Malkov99f3d292020-06-26 13:24:45 +0100207 val baselineOffset = HeightToFirstLine.toIntPx()
208 textPlaceY = baselineOffset - firstTextBaseline - SnackbarVerticalPadding.toIntPx()
209 val minContainerHeight = SnackbarMinHeightTwoLines.toIntPx()
Matvei Malkovb814df02019-11-21 15:58:03 +0000210 val contentHeight = textPlaceY + textPlaceable.height
211 containerHeight = max(minContainerHeight, contentHeight)
212 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
213 }
214
215 layout(constraints.maxWidth, containerHeight) {
George Mount8f237572020-04-30 12:08:30 -0700216 textPlaceable.place(0, textPlaceY)
Matvei Malkovb814df02019-11-21 15:58:03 +0000217 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
218 }
219 }
220}
221
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000222/**
223 * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
224 * Given that [Snackbar]s have an 'inverted' theme, i.e in a light theme they appear dark, and
225 * in a dark theme they appear light, just using [ColorPalette.primary] will not work, and has
226 * incorrect contrast.
227 *
Louis Pullen-Freilich644c22e2020-03-02 16:05:01 +0000228 * If your light theme has a corresponding dark theme, you should instead directly use
229 * [ColorPalette.primary] from the dark theme when in a light theme, and use
230 * [ColorPalette.primaryVariant] from the dark theme when in a dark theme.
231 *
232 * When in a light theme, this function applies a color overlay to [ColorPalette.primary] from
233 * [colors] to attempt to reduce the contrast, and when in a dark theme this function uses
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000234 * [ColorPalette.primaryVariant].
235 *
236 * @param colors the [ColorPalette] to calculate the Snackbar primary color for
237 */
238fun snackbarPrimaryColorFor(colors: ColorPalette): Color {
239 return if (colors.isLight) {
240 val primary = colors.primary
241 val overlayColor = colors.surface.copy(alpha = 0.6f)
242
243 overlayColor.compositeOver(primary)
244 } else {
245 colors.primaryVariant
246 }
Matvei Malkovb814df02019-11-21 15:58:03 +0000247}
248
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000249private const val SnackbarOverlayAlpha = 0.8f
Matvei Malkovb814df02019-11-21 15:58:03 +0000250
Matvei Malkovb814df02019-11-21 15:58:03 +0000251private val HeightToFirstLine = 30.dp
252private val HorizontalSpacing = 16.dp
253private val HorizontalSpacingButtonSide = 8.dp
254private val SeparateButtonExtraY = 8.dp
Matvei Malkov99f3d292020-06-26 13:24:45 +0100255private val SnackbarVerticalPadding = 6.dp
Matvei Malkovb814df02019-11-21 15:58:03 +0000256private val TextEndExtraSpacing = 8.dp
Matvei Malkov99f3d292020-06-26 13:24:45 +0100257private val LongButtonVerticalOffset = 18.dp
258private val SnackbarMinHeightOneLine = 48.dp - SnackbarVerticalPadding * 2
259private val SnackbarMinHeightTwoLines = 68.dp - SnackbarVerticalPadding * 2