[go: nahoru, domu]

blob: e147f8bfe531ecf3d43f161399c256a060167310 [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
20import androidx.compose.unaryPlus
21import androidx.ui.core.CurrentTextStyleProvider
22import androidx.ui.core.FirstBaseline
23import androidx.ui.core.IntPx
24import androidx.ui.core.LastBaseline
25import androidx.ui.core.Layout
26import androidx.ui.core.Modifier
27import androidx.ui.core.Text
28import androidx.ui.core.coerceAtLeast
29import androidx.ui.core.dp
30import androidx.ui.core.ipx
31import androidx.ui.core.max
32import androidx.ui.foundation.shape.DrawShape
33import androidx.ui.foundation.shape.corner.RoundedCornerShape
34import androidx.ui.graphics.Color
35import androidx.ui.layout.AlignmentLineOffset
36import androidx.ui.layout.Column
37import androidx.ui.layout.Container
Adam Powell712dc992019-12-04 12:48:30 -080038import androidx.ui.layout.LayoutExpandedWidth
39import androidx.ui.layout.LayoutGravity
40import androidx.ui.layout.LayoutPadding
Matvei Malkovb814df02019-11-21 15:58:03 +000041import androidx.ui.material.surface.Surface
42
43/**
44 * Snackbars provide brief messages about app processes at the bottom of the screen.
45 *
46 * Snackbars inform users of a process that an app has performed or will perform. They appear
47 * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
48 * and they don’t require user input to disappear.
49 *
50 * A snackbar can contain a single action. Because they disappear automatically, the action
51 * shouldn’t be “Dismiss” or “Cancel.”
52 *
53 * @sample androidx.ui.material.samples.SimpleSnackbar
54 *
55 * @param text information about a process that an app has performed or will perform
56 * @param actionText action name in the snackbar. If null, there will be text label with no
57 * action button
58 * @param onActionClick lambda to be invoked when the action is clicked
59 * @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: String,
66 actionText: String? = null,
67 onActionClick: (() -> Unit)? = null,
68 modifier: Modifier = Modifier.None,
69 actionOnNewLine: Boolean = false
70) {
71 val actionSlot: @Composable() (() -> Unit)? =
72 if (actionText != null) {
73 @Composable {
74 Button(
75 text = actionText,
76 onClick = onActionClick,
77 style = TextButtonStyle(
78 // TODO: remove this when primary light variant is figured out
79 contentColor = makePrimaryVariantLight((+MaterialTheme.colors()).primary)
80 )
81 )
82 }
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) {
123 val colors = +MaterialTheme.colors()
124 Surface(
125 modifier = modifier,
126 shape = SnackbarShape,
127 elevation = SnackbarElevation,
128 color = colors.surface
129 ) {
130 val textStyle = (+MaterialTheme.typography()).body2.copy(color = colors.surface)
131 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(
Adam Powell712dc992019-12-04 12:48:30 -0800148 text, modifier = LayoutPadding(left = HorizontalSpacing, right = 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 Powell712dc992019-12-04 12:48:30 -0800171 modifier = LayoutExpandedWidth wraps LayoutPadding(
Matvei Malkovb814df02019-11-21 15:58:03 +0000172 left = HorizontalSpacing,
173 right = HorizontalSpacingButtonSide,
174 bottom = SeparateButtonExtraY
175 )
176 ) {
177 AlignmentLineOffset(alignmentLine = LastBaseline, after = LongButtonVerticalOffset) {
178 AlignmentLineOffset(alignmentLine = FirstBaseline, before = HeightToFirstLine) {
Adam Powell712dc992019-12-04 12:48:30 -0800179 Container(LayoutPadding(right = 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(
192 text, button,
Adam Powell712dc992019-12-04 12:48:30 -0800193 modifier = LayoutPadding(left = HorizontalSpacing, right = HorizontalSpacingButtonSide)
Matvei Malkovb814df02019-11-21 15:58:03 +0000194 ) { measurables, constraints ->
195 require(measurables[text].size == 1) {
196 "text for Snackbar expected to have exactly only one child"
197 }
198 require(measurables[button].size == 1) {
199 "button for Snackbar expected to have exactly only one child"
200 }
201 val buttonPlaceable = measurables[button].first().measure(constraints)
202 val textMaxWidth =
203 (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
204 .coerceAtLeast(constraints.minWidth)
205 val textPlaceable = measurables[text].first()
206 .measure(constraints.copy(minHeight = IntPx.Zero, maxWidth = textMaxWidth))
207
208 val firstTextBaseline =
209 requireNotNull(textPlaceable[FirstBaseline]) { "No baselines for text" }
210 val lastTextBaseline =
211 requireNotNull(textPlaceable[LastBaseline]) { "No baselines for text" }
212 val baselineOffset = HeightToFirstLine.toIntPx()
213 val isOneLine = firstTextBaseline == lastTextBaseline
214 val textPlaceY = baselineOffset - firstTextBaseline
215 val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
216
217 val containerHeight: IntPx
218 val buttonPlaceY: IntPx
219 if (isOneLine) {
220 val minContainerHeight = MinHeightOneLine.toIntPx()
221 val contentHeight = buttonPlaceable.height + SingleTextYPadding.toIntPx() * 2
222 containerHeight = max(minContainerHeight, contentHeight)
223 val buttonBaseline = buttonPlaceable.get(FirstBaseline)
224 buttonPlaceY =
225 buttonBaseline?.let { baselineOffset - it } ?: SingleTextYPadding.toIntPx()
226 } else {
227 val minContainerHeight = MinHeightTwoLines.toIntPx()
228 val contentHeight = textPlaceY + textPlaceable.height
229 containerHeight = max(minContainerHeight, contentHeight)
230 buttonPlaceY = (containerHeight - buttonPlaceable.height) / 2
231 }
232
233 layout(constraints.maxWidth, containerHeight) {
234 textPlaceable.place(0.ipx, textPlaceY)
235 buttonPlaceable.place(buttonPlaceX, buttonPlaceY)
236 }
237 }
238}
239
240// TODO: remove this when primary light variant is figured out in MaterialTheme
241private fun makePrimaryVariantLight(primary: Color): Color {
242 val blendColor = Color.White.copy(alpha = 0.6f)
243 return Color(
244 red = blendColor.red * blendColor.alpha + primary.red * (1 - blendColor.alpha),
245 green = blendColor.green * blendColor.alpha + primary.green * (1 - blendColor.alpha),
246 blue = blendColor.blue * blendColor.alpha + primary.blue * (1 - blendColor.alpha)
247 )
248}
249
250private val TextMaxLines = 2
251private val SnackbarOverlayAlpha = 0.8f
252private val SnackbarShape = RoundedCornerShape(4.dp)
253private val SnackbarElevation = 6.dp
254
255private val MinHeightOneLine = 48.dp
256private val MinHeightTwoLines = 68.dp
257private val HeightToFirstLine = 30.dp
258private val HorizontalSpacing = 16.dp
259private val HorizontalSpacingButtonSide = 8.dp
260private val SeparateButtonExtraY = 8.dp
261private val SingleTextYPadding = 6.dp
262private val TextEndExtraSpacing = 8.dp
263private val LongButtonVerticalOffset = 18.dp