Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package androidx.ui.material |
| 18 | |
| 19 | import androidx.compose.Composable |
Mihai Popa | 5b7a6bb | 2020-04-08 20:11:03 +0100 | [diff] [blame] | 20 | import androidx.ui.core.Alignment |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 21 | import androidx.ui.core.AlignmentLine |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 22 | import androidx.ui.core.Layout |
| 23 | import androidx.ui.core.Modifier |
Mihai Popa | 4d1d814 | 2020-06-08 16:08:16 +0100 | [diff] [blame] | 24 | import androidx.ui.core.id |
| 25 | import androidx.ui.core.layoutId |
Louis Pullen-Freilich | ddda7be | 2020-07-17 18:28:12 +0100 | [diff] [blame] | 26 | import androidx.compose.foundation.Box |
| 27 | import androidx.compose.foundation.ProvideTextStyle |
Louis Pullen-Freilich | 4dc4dac | 2020-07-22 14:39:14 +0100 | [diff] [blame^] | 28 | import androidx.compose.ui.graphics.Color |
| 29 | import androidx.compose.ui.graphics.Shape |
| 30 | import androidx.compose.ui.graphics.compositeOver |
Louis Pullen-Freilich | 623e405 | 2020-07-19 20:24:03 +0100 | [diff] [blame] | 31 | import androidx.compose.foundation.layout.Column |
| 32 | import androidx.compose.foundation.layout.fillMaxWidth |
| 33 | import androidx.compose.foundation.layout.padding |
| 34 | import androidx.compose.foundation.layout.relativePaddingFrom |
Louis Pullen-Freilich | ca6eca2 | 2020-07-20 00:31:45 +0100 | [diff] [blame] | 35 | import androidx.compose.foundation.text.FirstBaseline |
| 36 | import androidx.compose.foundation.text.LastBaseline |
Andrey Kulikov | 6e28e97 | 2020-03-25 12:31:24 +0000 | [diff] [blame] | 37 | import androidx.ui.unit.Dp |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 38 | import androidx.ui.unit.dp |
George Mount | 8f23757 | 2020-04-30 12:08:30 -0700 | [diff] [blame] | 39 | import kotlin.math.max |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 40 | |
| 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-Freilich | 644c22e | 2020-03-02 16:05:01 +0000 | [diff] [blame] | 48 | * A Snackbar can contain a single action. Because they disappear automatically, the action |
| 49 | * shouldn't be "Dismiss" or "Cancel". |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 50 | * |
| 51 | * @sample androidx.ui.material.samples.SimpleSnackbar |
| 52 | * |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 53 | * @param text text component to show information about a process that an app has performed or |
| 54 | * will perform |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 55 | * @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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 58 | * @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 Kulikov | 6e28e97 | 2020-03-25 12:31:24 +0000 | [diff] [blame] | 61 | * @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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 64 | */ |
| 65 | @Composable |
| 66 | fun Snackbar( |
Louis Pullen-Freilich | 3a54b94 | 2020-05-07 13:23:03 +0100 | [diff] [blame] | 67 | text: @Composable () -> Unit, |
| 68 | action: @Composable (() -> Unit)? = null, |
Adam Powell | b6d8db2 | 2020-04-02 12:40:03 -0700 | [diff] [blame] | 69 | modifier: Modifier = Modifier, |
Andrey Kulikov | 6e28e97 | 2020-03-25 12:31:24 +0000 | [diff] [blame] | 70 | actionOnNewLine: Boolean = false, |
| 71 | shape: Shape = MaterialTheme.shapes.small, |
| 72 | elevation: Dp = 6.dp |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 73 | ) { |
Louis Pullen-Freilich | b259185 | 2020-03-23 19:00:09 +0000 | [diff] [blame] | 74 | val colors = MaterialTheme.colors |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 75 | // 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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 78 | Surface( |
| 79 | modifier = modifier, |
Andrey Kulikov | 6e28e97 | 2020-03-25 12:31:24 +0000 | [diff] [blame] | 80 | shape = shape, |
| 81 | elevation = elevation, |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 82 | color = snackbarColor, |
| 83 | contentColor = colors.surface |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 84 | ) { |
Louis Pullen-Freilich | 9831863 | 2020-03-27 00:32:05 +0000 | [diff] [blame] | 85 | ProvideEmphasis(EmphasisAmbient.current.high) { |
Louis Pullen-Freilich | b259185 | 2020-03-23 19:00:09 +0000 | [diff] [blame] | 86 | val textStyle = MaterialTheme.typography.body2 |
Louis Pullen-Freilich | 0d04438 | 2020-03-17 22:43:35 +0000 | [diff] [blame] | 87 | ProvideTextStyle(value = textStyle) { |
Louis Pullen-Freilich | d3c2153 | 2020-03-02 19:23:23 +0000 | [diff] [blame] | 88 | when { |
| 89 | action == null -> TextOnlySnackbar(text) |
| 90 | actionOnNewLine -> NewLineButtonSnackbar(text, action) |
| 91 | else -> OneRowSnackbar(text, action) |
| 92 | } |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 93 | } |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | @Composable |
Louis Pullen-Freilich | 3a54b94 | 2020-05-07 13:23:03 +0100 | [diff] [blame] | 99 | private fun TextOnlySnackbar(text: @Composable () -> Unit) { |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 100 | Layout( |
Matvei Malkov | 59bac36 | 2020-02-13 20:23:13 +0000 | [diff] [blame] | 101 | text, |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 102 | modifier = Modifier.padding( |
| 103 | start = HorizontalSpacing, |
| 104 | end = HorizontalSpacing, |
| 105 | top = SnackbarVerticalPadding, |
| 106 | bottom = SnackbarVerticalPadding |
| 107 | ) |
Anastasia Soboleva | 5e382dd | 2020-06-17 21:51:38 +0100 | [diff] [blame] | 108 | ) { measurables, constraints -> |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 109 | require(measurables.size == 1) { |
| 110 | "text for Snackbar expected to have exactly only one child" |
| 111 | } |
| 112 | val textPlaceable = measurables.first().measure(constraints) |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 113 | 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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 117 | |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 118 | 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 Mount | 8f23757 | 2020-04-30 12:08:30 -0700 | [diff] [blame] | 127 | textPlaceable.place(0, textPlaceY) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | @Composable |
| 133 | private fun NewLineButtonSnackbar( |
Louis Pullen-Freilich | 3a54b94 | 2020-05-07 13:23:03 +0100 | [diff] [blame] | 134 | text: @Composable () -> Unit, |
| 135 | action: @Composable () -> Unit |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 136 | ) { |
| 137 | Column( |
Adam Powell | 999a89b | 2020-03-11 09:08:07 -0700 | [diff] [blame] | 138 | modifier = Modifier.fillMaxWidth() |
| 139 | .padding( |
| 140 | start = HorizontalSpacing, |
| 141 | end = HorizontalSpacingButtonSide, |
| 142 | bottom = SeparateButtonExtraY |
| 143 | ) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 144 | ) { |
Anastasia Soboleva | 57c818f | 2020-05-06 00:13:51 +0100 | [diff] [blame] | 145 | Box( |
| 146 | Modifier |
| 147 | .relativePaddingFrom(LastBaseline, after = LongButtonVerticalOffset) |
| 148 | .relativePaddingFrom(FirstBaseline, before = HeightToFirstLine) |
| 149 | .padding(end = HorizontalSpacingButtonSide), |
| 150 | children = text |
| 151 | ) |
Mihai Popa | 5b7a6bb | 2020-04-08 20:11:03 +0100 | [diff] [blame] | 152 | Box(Modifier.gravity(Alignment.End), children = action) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 153 | } |
| 154 | } |
| 155 | |
| 156 | @Composable |
| 157 | private fun OneRowSnackbar( |
Louis Pullen-Freilich | 3a54b94 | 2020-05-07 13:23:03 +0100 | [diff] [blame] | 158 | text: @Composable () -> Unit, |
| 159 | action: @Composable () -> Unit |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 160 | ) { |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 161 | val textTag = "text" |
| 162 | val actionTag = "action" |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 163 | Layout( |
Mihai Popa | 8c474e9 | 2019-12-10 17:43:20 +0000 | [diff] [blame] | 164 | { |
Mihai Popa | 4d1d814 | 2020-06-08 16:08:16 +0100 | [diff] [blame] | 165 | Box(Modifier.layoutId(textTag), children = text) |
| 166 | Box(Modifier.layoutId(actionTag), children = action) |
Mihai Popa | 8c474e9 | 2019-12-10 17:43:20 +0000 | [diff] [blame] | 167 | }, |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 168 | modifier = Modifier.padding( |
| 169 | start = HorizontalSpacing, |
| 170 | end = HorizontalSpacingButtonSide, |
| 171 | top = SnackbarVerticalPadding, |
| 172 | bottom = SnackbarVerticalPadding |
| 173 | ) |
Anastasia Soboleva | 5e382dd | 2020-06-17 21:51:38 +0100 | [diff] [blame] | 174 | ) { measurables, constraints -> |
Mihai Popa | 4d1d814 | 2020-06-08 16:08:16 +0100 | [diff] [blame] | 175 | val buttonPlaceable = measurables.first { it.id == actionTag }.measure(constraints) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 176 | val textMaxWidth = |
| 177 | (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx()) |
| 178 | .coerceAtLeast(constraints.minWidth) |
Mihai Popa | 4d1d814 | 2020-06-08 16:08:16 +0100 | [diff] [blame] | 179 | val textPlaceable = measurables.first { it.id == textTag }.measure( |
George Mount | 8f23757 | 2020-04-30 12:08:30 -0700 | [diff] [blame] | 180 | constraints.copy(minHeight = 0, maxWidth = textMaxWidth) |
Mihai Popa | 8c474e9 | 2019-12-10 17:43:20 +0000 | [diff] [blame] | 181 | ) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 182 | |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 183 | 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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 187 | val isOneLine = firstTextBaseline == lastTextBaseline |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 188 | val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width |
| 189 | |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 190 | val textPlaceY: Int |
George Mount | 8f23757 | 2020-04-30 12:08:30 -0700 | [diff] [blame] | 191 | val containerHeight: Int |
| 192 | val buttonPlaceY: Int |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 193 | if (isOneLine) { |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 194 | val minContainerHeight = SnackbarMinHeightOneLine.toIntPx() |
| 195 | val contentHeight = buttonPlaceable.height |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 196 | containerHeight = max(minContainerHeight, contentHeight) |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 197 | textPlaceY = (containerHeight - textPlaceable.height) / 2 |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 198 | val buttonBaseline = buttonPlaceable[FirstBaseline] |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 199 | buttonPlaceY = buttonBaseline.let { |
| 200 | if (it != AlignmentLine.Unspecified) { |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 201 | textPlaceY + firstTextBaseline - it |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 202 | } else { |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 203 | 0 |
Mihai Popa | 08c4795 | 2020-06-04 20:01:27 +0100 | [diff] [blame] | 204 | } |
| 205 | } |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 206 | } else { |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 207 | val baselineOffset = HeightToFirstLine.toIntPx() |
| 208 | textPlaceY = baselineOffset - firstTextBaseline - SnackbarVerticalPadding.toIntPx() |
| 209 | val minContainerHeight = SnackbarMinHeightTwoLines.toIntPx() |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 210 | val contentHeight = textPlaceY + textPlaceable.height |
| 211 | containerHeight = max(minContainerHeight, contentHeight) |
| 212 | buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2 |
| 213 | } |
| 214 | |
| 215 | layout(constraints.maxWidth, containerHeight) { |
George Mount | 8f23757 | 2020-04-30 12:08:30 -0700 | [diff] [blame] | 216 | textPlaceable.place(0, textPlaceY) |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 217 | buttonPlaceable.place(buttonPlaceX, buttonPlaceY) |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 222 | /** |
| 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-Freilich | 644c22e | 2020-03-02 16:05:01 +0000 | [diff] [blame] | 228 | * 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-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 234 | * [ColorPalette.primaryVariant]. |
| 235 | * |
| 236 | * @param colors the [ColorPalette] to calculate the Snackbar primary color for |
| 237 | */ |
| 238 | fun 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 Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 247 | } |
| 248 | |
Louis Pullen-Freilich | 68565b0 | 2020-02-26 16:40:01 +0000 | [diff] [blame] | 249 | private const val SnackbarOverlayAlpha = 0.8f |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 250 | |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 251 | private val HeightToFirstLine = 30.dp |
| 252 | private val HorizontalSpacing = 16.dp |
| 253 | private val HorizontalSpacingButtonSide = 8.dp |
| 254 | private val SeparateButtonExtraY = 8.dp |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 255 | private val SnackbarVerticalPadding = 6.dp |
Matvei Malkov | b814df0 | 2019-11-21 15:58:03 +0000 | [diff] [blame] | 256 | private val TextEndExtraSpacing = 8.dp |
Matvei Malkov | 99f3d29 | 2020-06-26 13:24:45 +0100 | [diff] [blame] | 257 | private val LongButtonVerticalOffset = 18.dp |
| 258 | private val SnackbarMinHeightOneLine = 48.dp - SnackbarVerticalPadding * 2 |
| 259 | private val SnackbarMinHeightTwoLines = 68.dp - SnackbarVerticalPadding * 2 |