[go: nahoru, domu]

blob: a9b0657fe071a346aa5a894529ff1fac4cd61c9d [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
Mihai Popa8c474e92019-12-10 17:43:20 +000024import androidx.ui.core.LayoutTagParentData
Matvei Malkovb814df02019-11-21 15:58:03 +000025import androidx.ui.core.Modifier
Mihai Popa8c474e92019-12-10 17:43:20 +000026import androidx.ui.core.ParentData
Matvei Malkovb814df02019-11-21 15:58:03 +000027import androidx.ui.core.Text
Mihai Popa8c474e92019-12-10 17:43:20 +000028import androidx.ui.core.tag
Matvei Malkovb814df02019-11-21 15:58:03 +000029import androidx.ui.foundation.shape.DrawShape
30import androidx.ui.foundation.shape.corner.RoundedCornerShape
31import androidx.ui.graphics.Color
32import androidx.ui.layout.AlignmentLineOffset
33import androidx.ui.layout.Column
34import androidx.ui.layout.Container
Adam Powell712dc992019-12-04 12:48:30 -080035import androidx.ui.layout.LayoutGravity
36import androidx.ui.layout.LayoutPadding
Adam Powell31c1ebd2020-01-09 09:48:24 -080037import androidx.ui.layout.LayoutWidth
Matvei Malkovb814df02019-11-21 15:58:03 +000038import androidx.ui.material.surface.Surface
George Mount842c8c12020-01-08 16:03:42 -080039import androidx.ui.unit.IntPx
40import androidx.ui.unit.dp
41import androidx.ui.unit.ipx
42import androidx.ui.unit.max
Matvei Malkovb814df02019-11-21 15:58:03 +000043
44/**
45 * Snackbars provide brief messages about app processes at the bottom of the screen.
46 *
47 * Snackbars inform users of a process that an app has performed or will perform. They appear
48 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
49 * and they don’t require user input to disappear.
50 *
51 * A snackbar can contain a single action. Because they disappear automatically, the action
52 * shouldn’t be “Dismiss” or “Cancel.”
53 *
54 * @sample androidx.ui.material.samples.SimpleSnackbar
55 *
56 * @param text information about a process that an app has performed or will perform
57 * @param actionText action name in the snackbar. If null, there will be text label with no
58 * action button
59 * @param onActionClick lambda to be invoked when the action is clicked
60 * @param modifier modifiers for the the Snackbar layout
61 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
62 * for action with long action text
63 */
64@Composable
65fun Snackbar(
66 text: String,
67 actionText: String? = null,
68 onActionClick: (() -> Unit)? = null,
69 modifier: Modifier = Modifier.None,
70 actionOnNewLine: Boolean = false
71) {
72 val actionSlot: @Composable() (() -> Unit)? =
73 if (actionText != null) {
74 @Composable {
Louis Pullen-Freiliche386f97dd2020-01-31 17:32:33 +000075 TextButton(
Matvei Malkovb814df02019-11-21 15:58:03 +000076 onClick = onActionClick,
Louis Pullen-Freiliche386f97dd2020-01-31 17:32:33 +000077 // TODO: remove this when primary light variant is figured out
78 contentColor = makePrimaryVariantLight(MaterialTheme.colors().primary)
79 ) {
80 Text(actionText)
81 }
Matvei Malkovb814df02019-11-21 15:58:03 +000082 }
83 } else {
84 null
85 }
86 Snackbar(
87 modifier = modifier,
88 actionOnNewLine = actionOnNewLine,
89 text = { Text(text, maxLines = TextMaxLines) },
90 action = actionSlot
91 )
92}
93
94/**
95 * Snackbars provide brief messages about app processes at the bottom of the screen.
96 *
97 * Snackbars inform users of a process that an app has performed or will perform. They appear
98 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
99 * and they don’t require user input to disappear.
100 *
101 * A snackbar can contain a single action. Because they disappear automatically, the action
102 * shouldn’t be “Dismiss” or “Cancel.”
103 *
104 * This version provides more granular control over the content of the Snackbar. Use it if you
105 * want to customize the content inside.
106 *
107 * @sample androidx.ui.material.samples.SlotsSnackbar
108 *
109 * @param text text component to show information about a process that an app has performed or
110 * will perform
111 * @param action action / button component to add as an action to the snackbar
112 * @param modifier modifiers for the the Snackbar layout
113 * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
114 * for action with long action text
115 */
116@Composable
117fun Snackbar(
118 text: @Composable() () -> Unit,
119 action: @Composable() (() -> Unit)? = null,
120 modifier: Modifier = Modifier.None,
121 actionOnNewLine: Boolean = false
122) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800123 val colors = MaterialTheme.colors()
Matvei Malkovb814df02019-11-21 15:58:03 +0000124 Surface(
125 modifier = modifier,
126 shape = SnackbarShape,
127 elevation = SnackbarElevation,
128 color = colors.surface
129 ) {
Leland Richardson7f848ab2019-12-12 13:43:41 -0800130 val textStyle = MaterialTheme.typography().body2.copy(color = colors.surface)
Matvei Malkovb814df02019-11-21 15:58:03 +0000131 DrawShape(
132 shape = SnackbarShape,
133 color = colors.onSurface.copy(alpha = SnackbarOverlayAlpha)
134 )
135 CurrentTextStyleProvider(value = textStyle) {
136 when {
137 action == null -> TextOnlySnackbar(text)
138 actionOnNewLine -> NewLineButtonSnackbar(text, action)
139 else -> OneRowSnackbar(text, action)
140 }
141 }
142 }
143}
144
145@Composable
146private fun TextOnlySnackbar(text: @Composable() () -> Unit) {
147 Layout(
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000148 text, modifier = LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacing)
Matvei Malkovb814df02019-11-21 15:58:03 +0000149 ) { measurables, constraints ->
150 require(measurables.size == 1) {
151 "text for Snackbar expected to have exactly only one child"
152 }
153 val textPlaceable = measurables.first().measure(constraints)
154 val firstBaseline = requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
155 val lastBaseline = requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
156
157 val minHeight = if (firstBaseline == lastBaseline) MinHeightOneLine else MinHeightTwoLines
158 layout(constraints.maxWidth, max(minHeight.toIntPx(), textPlaceable.height)) {
159 val textPlaceY = HeightToFirstLine.toIntPx() - firstBaseline
160 textPlaceable.place(0.ipx, textPlaceY)
161 }
162 }
163}
164
165@Composable
166private fun NewLineButtonSnackbar(
167 text: @Composable() () -> Unit,
168 button: @Composable() () -> Unit
169) {
170 Column(
Adam Powell31c1ebd2020-01-09 09:48:24 -0800171 modifier = LayoutWidth.Fill + LayoutPadding(
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000172 start = HorizontalSpacing,
173 end = HorizontalSpacingButtonSide,
Matvei Malkovb814df02019-11-21 15:58:03 +0000174 bottom = SeparateButtonExtraY
175 )
176 ) {
177 AlignmentLineOffset(alignmentLine = LastBaseline, after = LongButtonVerticalOffset) {
178 AlignmentLineOffset(alignmentLine = FirstBaseline, before = HeightToFirstLine) {
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000179 Container(LayoutPadding(end = HorizontalSpacingButtonSide), children = text)
Matvei Malkovb814df02019-11-21 15:58:03 +0000180 }
181 }
Adam Powell712dc992019-12-04 12:48:30 -0800182 Container(modifier = LayoutGravity.End, children = button)
Matvei Malkovb814df02019-11-21 15:58:03 +0000183 }
184}
185
186@Composable
187private fun OneRowSnackbar(
188 text: @Composable() () -> Unit,
189 button: @Composable() () -> Unit
190) {
191 Layout(
Mihai Popa8c474e92019-12-10 17:43:20 +0000192 {
193 ParentData(
194 object : LayoutTagParentData {
195 override val tag: Any = "text"
196 },
197 text
198 )
199 ParentData(
200 object : LayoutTagParentData {
201 override val tag: Any = "button"
202 },
203 button
204 )
205 },
Anastasia Soboleva24bacea2020-02-06 19:10:26 +0000206 modifier = LayoutPadding(start = HorizontalSpacing, end = HorizontalSpacingButtonSide)
Matvei Malkovb814df02019-11-21 15:58:03 +0000207 ) { measurables, constraints ->
Mihai Popa8c474e92019-12-10 17:43:20 +0000208 val buttonPlaceable = measurables.first { it.tag == "button" }.measure(constraints)
Matvei Malkovb814df02019-11-21 15:58:03 +0000209 val textMaxWidth =
210 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
211 .coerceAtLeast(constraints.minWidth)
Mihai Popa8c474e92019-12-10 17:43:20 +0000212 val textPlaceable = measurables.first { it.tag == "text" }.measure(
213 constraints.copy(minHeight = IntPx.Zero, maxWidth = textMaxWidth)
214 )
Matvei Malkovb814df02019-11-21 15:58:03 +0000215
216 val firstTextBaseline =
217 requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
218 val lastTextBaseline =
219 requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
220 val baselineOffset = HeightToFirstLine.toIntPx()
221 val isOneLine = firstTextBaseline == lastTextBaseline
222 val textPlaceY = baselineOffset - firstTextBaseline
223 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
224
225 val containerHeight: IntPx
226 val buttonPlaceY: IntPx
227 if (isOneLine) {
228 val minContainerHeight = MinHeightOneLine.toIntPx()
229 val contentHeight = buttonPlaceable.height + SingleTextYPadding.toIntPx() * 2
230 containerHeight = max(minContainerHeight, contentHeight)
231 val buttonBaseline = buttonPlaceable.get(FirstBaseline)
232 buttonPlaceY =
233 buttonBaseline?.let { baselineOffset - it } ?: SingleTextYPadding.toIntPx()
234 } else {
235 val minContainerHeight = MinHeightTwoLines.toIntPx()
236 val contentHeight = textPlaceY + textPlaceable.height
237 containerHeight = max(minContainerHeight, contentHeight)
238 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
239 }
240
241 layout(constraints.maxWidth, containerHeight) {
242 textPlaceable.place(0.ipx, textPlaceY)
243 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
244 }
245 }
246}
247
248// TODO: remove this when primary light variant is figured out in MaterialTheme
249private fun makePrimaryVariantLight(primary: Color): Color {
250 val blendColor = Color.White.copy(alpha = 0.6f)
251 return Color(
252 red = blendColor.red * blendColor.alpha + primary.red * (1 - blendColor.alpha),
253 green = blendColor.green * blendColor.alpha + primary.green * (1 - blendColor.alpha),
254 blue = blendColor.blue * blendColor.alpha + primary.blue * (1 - blendColor.alpha)
255 )
256}
257
258private val TextMaxLines = 2
259private val SnackbarOverlayAlpha = 0.8f
260private val SnackbarShape = RoundedCornerShape(4.dp)
261private val SnackbarElevation = 6.dp
262
263private val MinHeightOneLine = 48.dp
264private val MinHeightTwoLines = 68.dp
265private val HeightToFirstLine = 30.dp
266private val HorizontalSpacing = 16.dp
267private val HorizontalSpacingButtonSide = 8.dp
268private val SeparateButtonExtraY = 8.dp
269private val SingleTextYPadding = 6.dp
270private val TextEndExtraSpacing = 8.dp
271private val LongButtonVerticalOffset = 18.dp