[go: nahoru, domu]

blob: f540daa9ef84e8ae5e1b333eb6950bf295346b25 [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
22import androidx.ui.core.IntPx
23import androidx.ui.core.LastBaseline
24import androidx.ui.core.Layout
25import androidx.ui.core.Modifier
26import androidx.ui.core.Text
27import androidx.ui.core.coerceAtLeast
28import androidx.ui.core.dp
29import androidx.ui.core.ipx
30import androidx.ui.core.max
31import androidx.ui.foundation.shape.DrawShape
32import androidx.ui.foundation.shape.corner.RoundedCornerShape
33import androidx.ui.graphics.Color
34import androidx.ui.layout.AlignmentLineOffset
35import androidx.ui.layout.Column
36import androidx.ui.layout.Container
Adam Powell712dc992019-12-04 12:48:30 -080037import androidx.ui.layout.LayoutExpandedWidth
38import androidx.ui.layout.LayoutGravity
39import androidx.ui.layout.LayoutPadding
Matvei Malkovb814df02019-11-21 15:58:03 +000040import androidx.ui.material.surface.Surface
41
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 *
49 * A snackbar can contain a single action. Because they disappear automatically, the action
50 * shouldn’t be “Dismiss” or “Cancel.”
51 *
52 * @sample androidx.ui.material.samples.SimpleSnackbar
53 *
54 * @param text information about a process that an app has performed or will perform
55 * @param actionText action name in the snackbar. If null, there will be text label with no
56 * action button
57 * @param onActionClick lambda to be invoked when the action is clicked
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
61 */
62@Composable
63fun Snackbar(
64 text: String,
65 actionText: String? = null,
66 onActionClick: (() -> Unit)? = null,
67 modifier: Modifier = Modifier.None,
68 actionOnNewLine: Boolean = false
69) {
70 val actionSlot: @Composable() (() -> Unit)? =
71 if (actionText != null) {
72 @Composable {
73 Button(
74 text = actionText,
75 onClick = onActionClick,
76 style = TextButtonStyle(
77 // TODO: remove this when primary light variant is figured out
Leland Richardson7f848ab2019-12-12 13:43:41 -080078 contentColor = makePrimaryVariantLight(MaterialTheme.colors().primary)
Matvei Malkovb814df02019-11-21 15:58:03 +000079 )
80 )
81 }
82 } else {
83 null
84 }
85 Snackbar(
86 modifier = modifier,
87 actionOnNewLine = actionOnNewLine,
88 text = { Text(text, maxLines = TextMaxLines) },
89 action = actionSlot
90 )
91}
92
93/**
94 * Snackbars provide brief messages about app processes at the bottom of the screen.
95 *
96 * Snackbars inform users of a process that an app has performed or will perform. They appear
97 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
98 * and they don’t require user input to disappear.
99 *
100 * A snackbar can contain a single action. Because they disappear automatically, the action
101 * shouldn’t be “Dismiss” or “Cancel.”
102 *
103 * This version provides more granular control over the content of the Snackbar. Use it if you
104 * want to customize the content inside.
105 *
106 * @sample androidx.ui.material.samples.SlotsSnackbar
107 *
108 * @param text text component to show information about a process that an app has performed or
109 * will perform
110 * @param action action / button component to add as an action to the snackbar
111 * @param modifier modifiers for the the Snackbar layout
112 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
113 * for action with long action text
114 */
115@Composable
116fun Snackbar(
117 text: @Composable() () -> Unit,
118 action: @Composable() (() -> Unit)? = null,
119 modifier: Modifier = Modifier.None,
120 actionOnNewLine: Boolean = false
121) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800122 val colors = MaterialTheme.colors()
Matvei Malkovb814df02019-11-21 15:58:03 +0000123 Surface(
124 modifier = modifier,
125 shape = SnackbarShape,
126 elevation = SnackbarElevation,
127 color = colors.surface
128 ) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800129 val textStyle = MaterialTheme.typography().body2.copy(color = colors.surface)
Matvei Malkovb814df02019-11-21 15:58:03 +0000130 DrawShape(
131 shape = SnackbarShape,
132 color = colors.onSurface.copy(alpha = SnackbarOverlayAlpha)
133 )
134 CurrentTextStyleProvider(value = textStyle) {
135 when {
136 action == null -> TextOnlySnackbar(text)
137 actionOnNewLine -> NewLineButtonSnackbar(text, action)
138 else -> OneRowSnackbar(text, action)
139 }
140 }
141 }
142}
143
144@Composable
145private fun TextOnlySnackbar(text: @Composable() () -> Unit) {
146 Layout(
Adam Powell712dc992019-12-04 12:48:30 -0800147 text, modifier = LayoutPadding(left = HorizontalSpacing, right = HorizontalSpacing)
Matvei Malkovb814df02019-11-21 15:58:03 +0000148 ) { measurables, constraints ->
149 require(measurables.size == 1) {
150 "text for Snackbar expected to have exactly only one child"
151 }
152 val textPlaceable = measurables.first().measure(constraints)
153 val firstBaseline = requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
154 val lastBaseline = requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
155
156 val minHeight = if (firstBaseline == lastBaseline) MinHeightOneLine else MinHeightTwoLines
157 layout(constraints.maxWidth, max(minHeight.toIntPx(), textPlaceable.height)) {
158 val textPlaceY = HeightToFirstLine.toIntPx() - firstBaseline
159 textPlaceable.place(0.ipx, textPlaceY)
160 }
161 }
162}
163
164@Composable
165private fun NewLineButtonSnackbar(
166 text: @Composable() () -> Unit,
167 button: @Composable() () -> Unit
168) {
169 Column(
Adam Powelldd723062019-12-04 13:52:59 -0800170 modifier = LayoutExpandedWidth + LayoutPadding(
Matvei Malkovb814df02019-11-21 15:58:03 +0000171 left = HorizontalSpacing,
172 right = HorizontalSpacingButtonSide,
173 bottom = SeparateButtonExtraY
174 )
175 ) {
176 AlignmentLineOffset(alignmentLine = LastBaseline, after = LongButtonVerticalOffset) {
177 AlignmentLineOffset(alignmentLine = FirstBaseline, before = HeightToFirstLine) {
Adam Powell712dc992019-12-04 12:48:30 -0800178 Container(LayoutPadding(right = HorizontalSpacingButtonSide), children = text)
Matvei Malkovb814df02019-11-21 15:58:03 +0000179 }
180 }
Adam Powell712dc992019-12-04 12:48:30 -0800181 Container(modifier = LayoutGravity.End, children = button)
Matvei Malkovb814df02019-11-21 15:58:03 +0000182 }
183}
184
185@Composable
186private fun OneRowSnackbar(
187 text: @Composable() () -> Unit,
188 button: @Composable() () -> Unit
189) {
190 Layout(
191 text, button,
Adam Powell712dc992019-12-04 12:48:30 -0800192 modifier = LayoutPadding(left = HorizontalSpacing, right = HorizontalSpacingButtonSide)
Matvei Malkovb814df02019-11-21 15:58:03 +0000193 ) { measurables, constraints ->
194 require(measurables[text].size == 1) {
195 "text for Snackbar expected to have exactly only one child"
196 }
197 require(measurables[button].size == 1) {
198 "button for Snackbar expected to have exactly only one child"
199 }
200 val buttonPlaceable = measurables[button].first().measure(constraints)
201 val textMaxWidth =
202 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
203 .coerceAtLeast(constraints.minWidth)
204 val textPlaceable = measurables[text].first()
205 .measure(constraints.copy(minHeight = IntPx.Zero, maxWidth = textMaxWidth))
206
207 val firstTextBaseline =
208 requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
209 val lastTextBaseline =
210 requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
211 val baselineOffset = HeightToFirstLine.toIntPx()
212 val isOneLine = firstTextBaseline == lastTextBaseline
213 val textPlaceY = baselineOffset - firstTextBaseline
214 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
215
216 val containerHeight: IntPx
217 val buttonPlaceY: IntPx
218 if (isOneLine) {
219 val minContainerHeight = MinHeightOneLine.toIntPx()
220 val contentHeight = buttonPlaceable.height + SingleTextYPadding.toIntPx() * 2
221 containerHeight = max(minContainerHeight, contentHeight)
222 val buttonBaseline = buttonPlaceable.get(FirstBaseline)
223 buttonPlaceY =
224 buttonBaseline?.let { baselineOffset - it } ?: SingleTextYPadding.toIntPx()
225 } else {
226 val minContainerHeight = MinHeightTwoLines.toIntPx()
227 val contentHeight = textPlaceY + textPlaceable.height
228 containerHeight = max(minContainerHeight, contentHeight)
229 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
230 }
231
232 layout(constraints.maxWidth, containerHeight) {
233 textPlaceable.place(0.ipx, textPlaceY)
234 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
235 }
236 }
237}
238
239// TODO: remove this when primary light variant is figured out in MaterialTheme
240private fun makePrimaryVariantLight(primary: Color): Color {
241 val blendColor = Color.White.copy(alpha = 0.6f)
242 return Color(
243 red = blendColor.red * blendColor.alpha + primary.red * (1 - blendColor.alpha),
244 green = blendColor.green * blendColor.alpha + primary.green * (1 - blendColor.alpha),
245 blue = blendColor.blue * blendColor.alpha + primary.blue * (1 - blendColor.alpha)
246 )
247}
248
249private val TextMaxLines = 2
250private val SnackbarOverlayAlpha = 0.8f
251private val SnackbarShape = RoundedCornerShape(4.dp)
252private val SnackbarElevation = 6.dp
253
254private val MinHeightOneLine = 48.dp
255private val MinHeightTwoLines = 68.dp
256private val HeightToFirstLine = 30.dp
257private val HorizontalSpacing = 16.dp
258private val HorizontalSpacingButtonSide = 8.dp
259private val SeparateButtonExtraY = 8.dp
260private val SingleTextYPadding = 6.dp
261private val TextEndExtraSpacing = 8.dp
262private val LongButtonVerticalOffset = 18.dp