1 /*
<lambda>null2 * Copyright 2021 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 * https://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 com.google.accompanist.insets.ui
18
19 import androidx.compose.foundation.layout.ColumnScope
20 import androidx.compose.foundation.layout.PaddingValues
21 import androidx.compose.foundation.layout.calculateEndPadding
22 import androidx.compose.foundation.layout.calculateStartPadding
23 import androidx.compose.material.BottomAppBar
24 import androidx.compose.material.DrawerDefaults
25 import androidx.compose.material.FabPosition
26 import androidx.compose.material.FloatingActionButton
27 import androidx.compose.material.MaterialTheme
28 import androidx.compose.material.ModalDrawer
29 import androidx.compose.material.Scaffold
30 import androidx.compose.material.ScaffoldState
31 import androidx.compose.material.Snackbar
32 import androidx.compose.material.SnackbarHost
33 import androidx.compose.material.SnackbarHostState
34 import androidx.compose.material.Surface
35 import androidx.compose.material.TopAppBar
36 import androidx.compose.material.contentColorFor
37 import androidx.compose.material.rememberScaffoldState
38 import androidx.compose.runtime.Composable
39 import androidx.compose.runtime.CompositionLocalProvider
40 import androidx.compose.runtime.Immutable
41 import androidx.compose.runtime.ProvidableCompositionLocal
42 import androidx.compose.runtime.Stable
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.remember
46 import androidx.compose.runtime.setValue
47 import androidx.compose.runtime.staticCompositionLocalOf
48 import androidx.compose.ui.Modifier
49 import androidx.compose.ui.graphics.Color
50 import androidx.compose.ui.graphics.Shape
51 import androidx.compose.ui.layout.SubcomposeLayout
52 import androidx.compose.ui.unit.Dp
53 import androidx.compose.ui.unit.LayoutDirection
54 import androidx.compose.ui.unit.dp
55
56 /**
57 * Provides the current [Scaffold] content padding values.
58 */
59 public val LocalScaffoldPadding: ProvidableCompositionLocal<PaddingValues> =
60 staticCompositionLocalOf { PaddingValues(0.dp) }
61
62 /**
63 * A copy of [androidx.compose.material.Scaffold] which lays out [content] behind both the top bar
64 * content, and the bottom bar content. See [androidx.compose.material.Scaffold] for more
65 * information about the features provided.
66 *
67 * A sample which demonstrates how to use this layout to achieve a edge-to-edge layout is
68 * below:
69 *
70 * @sample com.google.accompanist.sample.insets.InsetsBasics
71 *
72 * @param modifier optional Modifier for the root of the [Scaffold]
73 * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
74 * variables to provide manual control over the drawer behavior, sizes of components, etc
75 * @param topBar top app bar of the screen. Consider using [TopAppBar].
76 * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
77 * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
78 * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
79 * @param floatingActionButton Main action button of your screen. Consider using
80 * [FloatingActionButton] for this slot.
81 * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
82 * possible options available.
83 * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
84 * [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
85 * [floatingActionButton].
86 * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right
87 * for RTL).
88 * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
89 * @param drawerShape shape of the drawer sheet (if set)
90 * @param drawerElevation drawer sheet elevation. This controls the size of the shadow
91 * below the drawer sheet (if set)
92 * @param drawerBackgroundColor background color to be used for the drawer sheet
93 * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to
94 * either the matching content color for [drawerBackgroundColor], or, if it is not a color from
95 * the theme, this will keep the same value set above this Surface.
96 * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
97 * @param backgroundColor background of the scaffold body
98 * @param contentColor color of the content in scaffold body. Defaults to either the matching
99 * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
100 * the same value set above this Surface.
101 * @param contentPadding Additional content padding to apply to the [content].
102 * @param content content of your screen. The lambda receives an [PaddingValues] that should be
103 * applied to the content root via Modifier.padding to properly offset top and bottom bars. If
104 * you're using VerticalScroller, apply this modifier to the child of the scroller, and not on
105 * the scroller itself.
106 */
107 @Deprecated(
108 """
109 accompanist/insets-ui has been deprecated.
110 This functionality has been upstreamed to Material.
111 For more migration information, please visit https://google.github.io/accompanist/insets/#migration
112 """
113 )
114 @Composable
Scaffoldnull115 public fun Scaffold(
116 modifier: Modifier = Modifier,
117 scaffoldState: ScaffoldState = rememberScaffoldState(),
118 topBar: @Composable () -> Unit = {},
<lambda>null119 bottomBar: @Composable () -> Unit = {},
<lambda>null120 snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
<lambda>null121 floatingActionButton: @Composable () -> Unit = {},
122 floatingActionButtonPosition: FabPosition = FabPosition.End,
123 isFloatingActionButtonDocked: Boolean = false,
124 drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
125 drawerGesturesEnabled: Boolean = true,
126 drawerShape: Shape = MaterialTheme.shapes.large,
127 drawerElevation: Dp = DrawerDefaults.Elevation,
128 drawerBackgroundColor: Color = MaterialTheme.colors.surface,
129 drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
130 drawerScrimColor: Color = DrawerDefaults.scrimColor,
131 backgroundColor: Color = MaterialTheme.colors.background,
132 contentColor: Color = contentColorFor(backgroundColor),
133 contentPadding: PaddingValues = LocalScaffoldPadding.current,
134 content: @Composable (PaddingValues) -> Unit
135 ) {
childModifiernull136 val child = @Composable { childModifier: Modifier ->
137 Surface(modifier = childModifier, color = backgroundColor, contentColor = contentColor) {
138 ScaffoldLayout(
139 isFabDocked = isFloatingActionButtonDocked,
140 fabPosition = floatingActionButtonPosition,
141 topBar = topBar,
142 content = content,
143 snackbar = { snackbarHost(scaffoldState.snackbarHostState) },
144 fab = floatingActionButton,
145 bottomBar = bottomBar,
146 contentPadding = contentPadding,
147 )
148 }
149 }
150
151 if (drawerContent != null) {
152 ModalDrawer(
153 modifier = modifier,
154 drawerState = scaffoldState.drawerState,
155 gesturesEnabled = drawerGesturesEnabled,
156 drawerContent = drawerContent,
157 drawerShape = drawerShape,
158 drawerElevation = drawerElevation,
159 drawerBackgroundColor = drawerBackgroundColor,
160 drawerContentColor = drawerContentColor,
161 scrimColor = drawerScrimColor,
<lambda>null162 content = { child(Modifier) }
163 )
164 } else {
165 child(modifier)
166 }
167 }
168
169 /**
170 * Layout for a [Scaffold]'s content.
171 *
172 * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
173 * @param fabPosition [FabPosition] for the FAB (if present)
174 * @param topBar the content to place at the top of the [Scaffold], typically a [TopAppBar]
175 * @param content the main 'body' of the [Scaffold]
176 * @param snackbar the [Snackbar] displayed on top of the [content]
177 * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
178 * and above the [bottomBar]
179 * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
180 * [content], typically a [BottomAppBar].
181 */
182 @Composable
ScaffoldLayoutnull183 private fun ScaffoldLayout(
184 isFabDocked: Boolean,
185 fabPosition: FabPosition,
186 topBar: @Composable () -> Unit,
187 content: @Composable (PaddingValues) -> Unit,
188 snackbar: @Composable () -> Unit,
189 fab: @Composable () -> Unit,
190 bottomBar: @Composable () -> Unit,
191 contentPadding: PaddingValues,
192 ) {
193 val innerPadding = remember { MutablePaddingValues() }
194
195 SubcomposeLayout { constraints ->
196 val layoutWidth = constraints.maxWidth
197 val layoutHeight = constraints.maxHeight
198
199 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
200
201 layout(layoutWidth, layoutHeight) {
202 val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map {
203 it.measure(looseConstraints)
204 }
205
206 val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
207
208 val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
209 it.measure(looseConstraints)
210 }
211
212 val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
213
214 val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab)
215 .mapNotNull { measurable ->
216 measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
217 }
218
219 val fabPlacement = if (fabPlaceables.isNotEmpty()) {
220 val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
221 val fabHeight = fabPlaceables.maxByOrNull { it.height }!!.height
222 // FAB distance from the left of the layout, taking into account LTR / RTL
223 val fabLeftOffset = if (fabPosition == FabPosition.End) {
224 if (layoutDirection == LayoutDirection.Ltr) {
225 layoutWidth - FabSpacing.roundToPx() - fabWidth
226 } else {
227 FabSpacing.roundToPx()
228 }
229 } else {
230 (layoutWidth - fabWidth) / 2
231 }
232
233 FabPlacement(
234 isDocked = isFabDocked,
235 left = fabLeftOffset,
236 width = fabWidth,
237 height = fabHeight
238 )
239 } else {
240 null
241 }
242
243 val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
244 CompositionLocalProvider(
245 LocalFabPlacement provides fabPlacement,
246 content = bottomBar
247 )
248 }.map { it.measure(looseConstraints) }
249
250 val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0
251 val fabOffsetFromBottom = fabPlacement?.let {
252 if (bottomBarHeight == 0) {
253 it.height +
254 FabSpacing.roundToPx() +
255 contentPadding.calculateBottomPadding().roundToPx()
256 } else {
257 if (isFabDocked) {
258 // Total height is the bottom bar height + half the FAB height
259 bottomBarHeight + (it.height / 2)
260 } else {
261 // Total height is the bottom bar height + the FAB height + the padding
262 // between the FAB and bottom bar
263 bottomBarHeight + it.height + FabSpacing.roundToPx()
264 }
265 }
266 }
267
268 val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
269 snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
270 } else {
271 0
272 }
273
274 // Update the inner padding
275 innerPadding.apply {
276 start = contentPadding.calculateStartPadding(LayoutDirection.Ltr)
277 top = topBarHeight.toDp() + contentPadding.calculateTopPadding()
278 end = contentPadding.calculateEndPadding(LayoutDirection.Ltr)
279 bottom = bottomBarHeight.toDp() + contentPadding.calculateBottomPadding()
280 }
281
282 val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
283 CompositionLocalProvider(LocalScaffoldPadding provides innerPadding) {
284 content(innerPadding)
285 }
286 }.map { it.measure(looseConstraints.copy(maxHeight = layoutHeight)) }
287
288 // Placing to control drawing order to match default elevation of each placeable
289
290 bodyContentPlaceables.forEach { it.place(0, 0) }
291 topBarPlaceables.forEach { it.place(0, 0) }
292 snackbarPlaceables.forEach {
293 it.place(0, layoutHeight - snackbarOffsetFromBottom)
294 }
295 // The bottom bar is always at the bottom of the layout
296 bottomBarPlaceables.forEach {
297 it.place(0, layoutHeight - bottomBarHeight)
298 }
299 // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
300 fabPlacement?.let { placement ->
301 fabPlaceables.forEach {
302 it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
303 }
304 }
305 }
306 }
307 }
308
309 /**
310 * Placement information for a [FloatingActionButton] inside a [Scaffold].
311 *
312 * @property isDocked whether the FAB should be docked with the bottom bar
313 * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
314 * support
315 * @property width the width of the FAB
316 * @property height the height of the FAB
317 */
318 @Immutable
319 internal class FabPlacement(
320 val isDocked: Boolean,
321 val left: Int,
322 val width: Int,
323 val height: Int
324 )
325
326 /**
327 * CompositionLocal containing a [FabPlacement] that is read by [BottomAppBar] to calculate notch
328 * location.
329 */
<lambda>null330 internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
331
332 // FAB spacing above the bottom bar / bottom of the Scaffold
333 private val FabSpacing = 16.dp
334
335 private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
336
337 @Stable
338 internal class MutablePaddingValues : PaddingValues {
339 var start: Dp by mutableStateOf(0.dp)
340 var top: Dp by mutableStateOf(0.dp)
341 var end: Dp by mutableStateOf(0.dp)
342 var bottom: Dp by mutableStateOf(0.dp)
343
calculateLeftPaddingnull344 override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp {
345 return when (layoutDirection) {
346 LayoutDirection.Ltr -> start
347 LayoutDirection.Rtl -> end
348 }
349 }
350
calculateTopPaddingnull351 override fun calculateTopPadding(): Dp = top
352
353 override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp {
354 return when (layoutDirection) {
355 LayoutDirection.Ltr -> end
356 LayoutDirection.Rtl -> start
357 }
358 }
359
calculateBottomPaddingnull360 override fun calculateBottomPadding(): Dp = bottom
361 }
362