[go: nahoru, domu]

blob: 39c99f7269228514fe0e5d242374ba0680b572ca [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
Matvei Malkovb814df02019-11-21 15:58:03 +000020import androidx.ui.core.CurrentTextStyleProvider
21import androidx.ui.core.FirstBaseline
Matvei Malkovb814df02019-11-21 15:58:03 +000022import androidx.ui.core.LastBaseline
23import androidx.ui.core.Layout
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000024import androidx.ui.core.LayoutTag
Matvei Malkovb814df02019-11-21 15:58:03 +000025import androidx.ui.core.Modifier
Mihai Popa8c474e92019-12-10 17:43:20 +000026import androidx.ui.core.tag
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000027import androidx.ui.foundation.Box
Matvei Malkovb814df02019-11-21 15:58:03 +000028import androidx.ui.foundation.shape.corner.RoundedCornerShape
29import androidx.ui.graphics.Color
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000030import androidx.ui.graphics.compositeOver
Matvei Malkovb814df02019-11-21 15:58:03 +000031import androidx.ui.layout.AlignmentLineOffset
32import androidx.ui.layout.Column
Adam Powell712dc992019-12-04 12:48:30 -080033import androidx.ui.layout.LayoutGravity
34import androidx.ui.layout.LayoutPadding
Adam Powell31c1ebd2020-01-09 09:48:24 -080035import androidx.ui.layout.LayoutWidth
Matvei Malkovb814df02019-11-21 15:58:03 +000036import androidx.ui.material.surface.Surface
George Mount842c8c12020-01-08 16:03:42 -080037import androidx.ui.unit.IntPx
38import androidx.ui.unit.dp
39import androidx.ui.unit.ipx
40import androidx.ui.unit.max
Matvei Malkovb814df02019-11-21 15:58:03 +000041
42/**
43 * Snackbars provide brief messages about app processes at the bottom of the screen.
44 *
45 * Snackbars inform users of a process that an app has performed or will perform. They appear
46 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
47 * and they don’t require user input to disappear.
48 *
Louis Pullen-Freilich644c22e2020-03-02 16:05:01 +000049 * A Snackbar can contain a single action. Because they disappear automatically, the action
50 * shouldn't be "Dismiss" or "Cancel".
Matvei Malkovb814df02019-11-21 15:58:03 +000051 *
52 * @sample androidx.ui.material.samples.SimpleSnackbar
53 *
Matvei Malkovb814df02019-11-21 15:58:03 +000054 * @param text text component to show information about a process that an app has performed or
55 * will perform
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000056 * @param action action / button component to add as an action to the snackbar. Consider using
57 * [snackbarPrimaryColorFor] as the color for the action, if you do not have a predefined color
58 * you wish to use instead.
Matvei Malkovb814df02019-11-21 15:58:03 +000059 * @param modifier modifiers for the the Snackbar layout
60 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
61 * for action with long action text
62 */
63@Composable
64fun Snackbar(
65 text: @Composable() () -> Unit,
66 action: @Composable() (() -> Unit)? = null,
67 modifier: Modifier = Modifier.None,
68 actionOnNewLine: Boolean = false
69) {
Leland Richardson7f848ab2019-12-12 13:43:41 -080070 val colors = MaterialTheme.colors()
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000071 // Snackbar has a background color of onSurface with an alpha applied blended on top of surface
72 val snackbarOverlayColor = colors.onSurface.copy(alpha = SnackbarOverlayAlpha)
73 val snackbarColor = snackbarOverlayColor.compositeOver(colors.surface)
Matvei Malkovb814df02019-11-21 15:58:03 +000074 Surface(
75 modifier = modifier,
76 shape = SnackbarShape,
77 elevation = SnackbarElevation,
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000078 color = snackbarColor,
79 contentColor = colors.surface
Matvei Malkovb814df02019-11-21 15:58:03 +000080 ) {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000081 val textStyle = MaterialTheme.typography().body2
Matvei Malkovb814df02019-11-21 15:58:03 +000082 CurrentTextStyleProvider(value = textStyle) {
83 when {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000084 action == null -> TextOnlySnackbar(text)
85 actionOnNewLine -> NewLineButtonSnackbar(text, action)
86 else -> OneRowSnackbar(text, action)
Matvei Malkovb814df02019-11-21 15:58:03 +000087 }
88 }
89 }
90}
91
92@Composable
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000093private fun TextOnlySnackbar(text: @Composable() () -> Unit) {
Matvei Malkovb814df02019-11-21 15:58:03 +000094 Layout(
Matvei Malkov59bac362020-02-13 20:23:13 +000095 text,
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +000096 modifier = LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacing)
Anastasia Soboleva9474ff82020-02-19 19:02:15 +000097 ) { measurables, constraints, _ ->
Matvei Malkovb814df02019-11-21 15:58:03 +000098 require(measurables.size == 1) {
99 "text for Snackbar expected to have exactly only one child"
100 }
101 val textPlaceable = measurables.first().measure(constraints)
102 val firstBaseline = requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
103 val lastBaseline = requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
104
105 val minHeight = if (firstBaseline == lastBaseline) MinHeightOneLine else MinHeightTwoLines
106 layout(constraints.maxWidth, max(minHeight.toIntPx(), textPlaceable.height)) {
107 val textPlaceY = HeightToFirstLine.toIntPx() - firstBaseline
108 textPlaceable.place(0.ipx, textPlaceY)
109 }
110 }
111}
112
113@Composable
114private fun NewLineButtonSnackbar(
115 text: @Composable() () -> Unit,
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000116 action: @Composable() () -> Unit
Matvei Malkovb814df02019-11-21 15:58:03 +0000117) {
118 Column(
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000119 modifier = LayoutWidth.Fill + LayoutPadding(
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000120 start = HorizontalSpacing,
121 end = HorizontalSpacingButtonSide,
Matvei Malkovb814df02019-11-21 15:58:03 +0000122 bottom = SeparateButtonExtraY
123 )
124 ) {
125 AlignmentLineOffset(alignmentLine = LastBaseline, after = LongButtonVerticalOffset) {
126 AlignmentLineOffset(alignmentLine = FirstBaseline, before = HeightToFirstLine) {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000127 Box(LayoutPadding(end = HorizontalSpacingButtonSide), children = text)
Matvei Malkovb814df02019-11-21 15:58:03 +0000128 }
129 }
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000130 Box(modifier = LayoutGravity.End, children = action)
Matvei Malkovb814df02019-11-21 15:58:03 +0000131 }
132}
133
134@Composable
135private fun OneRowSnackbar(
136 text: @Composable() () -> Unit,
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000137 action: @Composable() () -> Unit
Matvei Malkovb814df02019-11-21 15:58:03 +0000138) {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000139 val textTag = "text"
140 val actionTag = "action"
Matvei Malkovb814df02019-11-21 15:58:03 +0000141 Layout(
Mihai Popa8c474e92019-12-10 17:43:20 +0000142 {
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000143 Box(LayoutTag(textTag), children = text)
144 Box(LayoutTag(actionTag), children = action)
Mihai Popa8c474e92019-12-10 17:43:20 +0000145 },
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000146 modifier = LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacingButtonSide)
Anastasia Soboleva9474ff82020-02-19 19:02:15 +0000147 ) { measurables, constraints, _ ->
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000148 val buttonPlaceable = measurables.first { it.tag == actionTag }.measure(constraints)
Matvei Malkovb814df02019-11-21 15:58:03 +0000149 val textMaxWidth =
150 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
151 .coerceAtLeast(constraints.minWidth)
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000152 val textPlaceable = measurables.first { it.tag == textTag }.measure(
Mihai Popa8c474e92019-12-10 17:43:20 +0000153 constraints.copy(minHeight = IntPx.Zero, maxWidth = textMaxWidth)
154 )
Matvei Malkovb814df02019-11-21 15:58:03 +0000155
156 val firstTextBaseline =
157 requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
158 val lastTextBaseline =
159 requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
160 val baselineOffset = HeightToFirstLine.toIntPx()
161 val isOneLine = firstTextBaseline == lastTextBaseline
162 val textPlaceY = baselineOffset - firstTextBaseline
163 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
164
165 val containerHeight: IntPx
166 val buttonPlaceY: IntPx
167 if (isOneLine) {
168 val minContainerHeight = MinHeightOneLine.toIntPx()
169 val contentHeight = buttonPlaceable.height + SingleTextYPadding.toIntPx() * 2
170 containerHeight = max(minContainerHeight, contentHeight)
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000171 val buttonBaseline = buttonPlaceable[FirstBaseline]
Matvei Malkovb814df02019-11-21 15:58:03 +0000172 buttonPlaceY =
173 buttonBaseline?.let { baselineOffset - it } ?: SingleTextYPadding.toIntPx()
174 } else {
175 val minContainerHeight = MinHeightTwoLines.toIntPx()
176 val contentHeight = textPlaceY + textPlaceable.height
177 containerHeight = max(minContainerHeight, contentHeight)
178 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
179 }
180
181 layout(constraints.maxWidth, containerHeight) {
182 textPlaceable.place(0.ipx, textPlaceY)
183 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
184 }
185 }
186}
187
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000188/**
189 * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
190 * Given that [Snackbar]s have an 'inverted' theme, i.e in a light theme they appear dark, and
191 * in a dark theme they appear light, just using [ColorPalette.primary] will not work, and has
192 * incorrect contrast.
193 *
Louis Pullen-Freilich644c22e2020-03-02 16:05:01 +0000194 * If your light theme has a corresponding dark theme, you should instead directly use
195 * [ColorPalette.primary] from the dark theme when in a light theme, and use
196 * [ColorPalette.primaryVariant] from the dark theme when in a dark theme.
197 *
198 * When in a light theme, this function applies a color overlay to [ColorPalette.primary] from
199 * [colors] to attempt to reduce the contrast, and when in a dark theme this function uses
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000200 * [ColorPalette.primaryVariant].
201 *
202 * @param colors the [ColorPalette] to calculate the Snackbar primary color for
203 */
204fun snackbarPrimaryColorFor(colors: ColorPalette): Color {
205 return if (colors.isLight) {
206 val primary = colors.primary
207 val overlayColor = colors.surface.copy(alpha = 0.6f)
208
209 overlayColor.compositeOver(primary)
210 } else {
211 colors.primaryVariant
212 }
Matvei Malkovb814df02019-11-21 15:58:03 +0000213}
214
Louis Pullen-Freilich68565b02020-02-26 16:40:01 +0000215private const val SnackbarOverlayAlpha = 0.8f
Matvei Malkovb814df02019-11-21 15:58:03 +0000216private val SnackbarShape = RoundedCornerShape(4.dp)
217private val SnackbarElevation = 6.dp
218
219private val MinHeightOneLine = 48.dp
220private val MinHeightTwoLines = 68.dp
221private val HeightToFirstLine = 30.dp
222private val HorizontalSpacing = 16.dp
223private val HorizontalSpacingButtonSide = 8.dp
224private val SeparateButtonExtraY = 8.dp
225private val SingleTextYPadding = 6.dp
226private val TextEndExtraSpacing = 8.dp
227private val LongButtonVerticalOffset = 18.dp