1 /*
<lambda>null2 * 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
17 package androidx.compose.material
18
19 import androidx.compose.foundation.layout.ColumnScope
20 import androidx.compose.foundation.layout.PaddingValues
21 import androidx.compose.foundation.layout.WindowInsets
22 import androidx.compose.foundation.layout.asPaddingValues
23 import androidx.compose.foundation.layout.calculateEndPadding
24 import androidx.compose.foundation.layout.calculateStartPadding
25 import androidx.compose.foundation.layout.consumeWindowInsets
26 import androidx.compose.foundation.layout.exclude
27 import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.CompositionLocalProvider
30 import androidx.compose.runtime.Immutable
31 import androidx.compose.runtime.Stable
32 import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.mutableStateOf
34 import androidx.compose.runtime.remember
35 import androidx.compose.runtime.setValue
36 import androidx.compose.runtime.staticCompositionLocalOf
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.UiComposable
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.graphics.Shape
41 import androidx.compose.ui.layout.SubcomposeLayout
42 import androidx.compose.ui.unit.Dp
43 import androidx.compose.ui.unit.LayoutDirection
44 import androidx.compose.ui.unit.dp
45 import androidx.compose.ui.unit.offset
46 import androidx.compose.ui.util.fastForEach
47 import androidx.compose.ui.util.fastMap
48 import androidx.compose.ui.util.fastMaxBy
49 import kotlin.jvm.JvmInline
50
51 /**
52 * State for [Scaffold] composable component.
53 *
54 * Contains basic screen state, e.g. Drawer configuration, as well as sizes of components after
55 * layout has happened
56 *
57 * @param drawerState the drawer state
58 * @param snackbarHostState instance of [SnackbarHostState] to be used to show [Snackbar]s inside of
59 * the [Scaffold]
60 */
61 @Stable class ScaffoldState(val drawerState: DrawerState, val snackbarHostState: SnackbarHostState)
62
63 /**
64 * Creates a [ScaffoldState] with the default animation clock and memoizes it.
65 *
66 * @param drawerState the drawer state
67 * @param snackbarHostState instance of [SnackbarHostState] to be used to show [Snackbar]s inside of
68 * the [Scaffold]
69 */
70 @Composable
71 fun rememberScaffoldState(
72 drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
73 snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
<lambda>null74 ): ScaffoldState = remember { ScaffoldState(drawerState, snackbarHostState) }
75
76 /** The possible positions for a [FloatingActionButton] attached to a [Scaffold]. */
77 @JvmInline
78 value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
79 companion object {
80 /**
81 * Position FAB at the bottom of the screen at the start, above the [BottomAppBar] (if it
82 * exists)
83 */
84 val Start = FabPosition(0)
85
86 /**
87 * Position FAB at the bottom of the screen in the center, above the [BottomAppBar] (if it
88 * exists)
89 */
90 val Center = FabPosition(1)
91
92 /**
93 * Position FAB at the bottom of the screen at the end, above the [BottomAppBar] (if it
94 * exists)
95 */
96 val End = FabPosition(2)
97 }
98
toStringnull99 override fun toString(): String {
100 return when (this) {
101 Start -> "FabPosition.Start"
102 Center -> "FabPosition.Center"
103 else -> "FabPosition.End"
104 }
105 }
106 }
107
108 /**
109 * [Material Design layout](https://material.io/design/layout/understanding-layout.html)
110 *
111 * Scaffold implements the basic Material Design visual layout structure.
112 *
113 * This component provides API to put together several Material components to construct your screen,
114 * by ensuring proper layout strategy for them and collecting necessary data so these components
115 * will work together correctly.
116 *
117 * For similar components that implement different layout structures, see [BackdropScaffold], which
118 * uses a backdrop as the centerpiece of the screen, and [BottomSheetScaffold], which uses a
119 * persistent bottom sheet as the centerpiece of the screen.
120 *
121 * This particular overload provides ability to specify [WindowInsets]. Recommended value can be
122 * found in [ScaffoldDefaults.contentWindowInsets].
123 *
124 * Simple example of a Scaffold with [TopAppBar], [FloatingActionButton] and drawer:
125 *
126 * @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
127 *
128 * More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
129 * animates its shape when clicked:
130 *
131 * @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
132 *
133 * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. Scaffold state already have
134 * [ScaffoldState.snackbarHostState] when created.
135 *
136 * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
137 * @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues]
138 * params. Scaffold will take the insets into account from the top/bottom only if the [topBar]/
139 * [bottomBar] are not present, as the scaffold expect [topBar]/[bottomBar] to handle insets
140 * instead. Any insets consumed by other insets padding modifiers or [consumeWindowInsets] on a
141 * parent layout will be excluded from [contentWindowInsets].
142 * @param modifier optional Modifier for the root of the [Scaffold]
143 * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
144 * variables to provide manual control over the drawer behavior, sizes of components, etc
145 * @param topBar top app bar of the screen. Consider using [TopAppBar].
146 * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
147 * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
148 * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
149 * @param floatingActionButton Main action button of your screen. Consider using
150 * [FloatingActionButton] for this slot.
151 * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
152 * possible options available.
153 * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
154 * [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
155 * [floatingActionButton].
156 * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right for
157 * RTL).
158 * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
159 * @param drawerShape shape of the drawer sheet (if set)
160 * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
161 * drawer sheet (if set)
162 * @param drawerBackgroundColor background color to be used for the drawer sheet
163 * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
164 * the matching content color for [drawerBackgroundColor], or, if it is not a color from the
165 * theme, this will keep the same value set above this Surface.
166 * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
167 * @param backgroundColor background of the scaffold body
168 * @param contentColor color of the content in scaffold body. Defaults to either the matching
169 * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
170 * the same value set above this Surface.
171 * @param content content of your screen. The lambda receives an [PaddingValues] that should be
172 * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
173 * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
174 * the child of the scroll, and not on the scroll itself.
175 */
176 @Composable
Scaffoldnull177 fun Scaffold(
178 contentWindowInsets: WindowInsets,
179 modifier: Modifier = Modifier,
180 scaffoldState: ScaffoldState = rememberScaffoldState(),
181 topBar: @Composable () -> Unit = {},
182 bottomBar: @Composable () -> Unit = {},
183 snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
184 floatingActionButton: @Composable () -> Unit = {},
185 floatingActionButtonPosition: FabPosition = FabPosition.End,
186 isFloatingActionButtonDocked: Boolean = false,
187 drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
188 drawerGesturesEnabled: Boolean = true,
189 drawerShape: Shape = MaterialTheme.shapes.large,
190 drawerElevation: Dp = DrawerDefaults.Elevation,
191 drawerBackgroundColor: Color = MaterialTheme.colors.surface,
192 drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
193 drawerScrimColor: Color = DrawerDefaults.scrimColor,
194 backgroundColor: Color = MaterialTheme.colors.background,
195 contentColor: Color = contentColorFor(backgroundColor),
196 content: @Composable (PaddingValues) -> Unit
197 ) {
<lambda>null198 val safeInsets = remember(contentWindowInsets) { MutableWindowInsets(contentWindowInsets) }
199 val child =
childModifiernull200 @Composable { childModifier: Modifier ->
201 Surface(
202 modifier =
203 childModifier.onConsumedWindowInsetsChanged { consumedWindowInsets ->
204 // Exclude currently consumed window insets from user provided
205 // contentWindowInsets
206 safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets)
207 },
208 color = backgroundColor,
209 contentColor = contentColor
210 ) {
211 ScaffoldLayout(
212 isFabDocked = isFloatingActionButtonDocked,
213 fabPosition = floatingActionButtonPosition,
214 topBar = topBar,
215 content = content,
216 contentWindowInsets = safeInsets,
217 snackbar = { snackbarHost(scaffoldState.snackbarHostState) },
218 fab = floatingActionButton,
219 bottomBar = bottomBar
220 )
221 }
222 }
223
224 if (drawerContent != null) {
225 ModalDrawer(
226 modifier = modifier,
227 drawerState = scaffoldState.drawerState,
228 gesturesEnabled = drawerGesturesEnabled,
229 drawerContent = drawerContent,
230 drawerShape = drawerShape,
231 drawerElevation = drawerElevation,
232 drawerBackgroundColor = drawerBackgroundColor,
233 drawerContentColor = drawerContentColor,
234 scrimColor = drawerScrimColor,
<lambda>null235 content = { child(Modifier) }
236 )
237 } else {
238 child(modifier)
239 }
240 }
241
242 /**
243 * [Material Design layout](https://material.io/design/layout/understanding-layout.html)
244 *
245 * Scaffold implements the basic Material Design visual layout structure.
246 *
247 * This component provides API to put together several Material components to construct your screen,
248 * by ensuring proper layout strategy for them and collecting necessary data so these components
249 * will work together correctly.
250 *
251 * For similar components that implement different layout structures, see [BackdropScaffold], which
252 * uses a backdrop as the centerpiece of the screen, and [BottomSheetScaffold], which uses a
253 * persistent bottom sheet as the centerpiece of the screen.
254 *
255 * Simple example of a Scaffold with [TopAppBar], [FloatingActionButton] and drawer:
256 *
257 * @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
258 *
259 * More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
260 * animates its shape when clicked:
261 *
262 * @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
263 *
264 * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. Scaffold state already have
265 * [ScaffoldState.snackbarHostState] when created
266 *
267 * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
268 * @param modifier optional Modifier for the root of the [Scaffold]
269 * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
270 * variables to provide manual control over the drawer behavior, sizes of components, etc
271 * @param topBar top app bar of the screen. Consider using [TopAppBar].
272 * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
273 * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
274 * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
275 * @param floatingActionButton Main action button of your screen. Consider using
276 * [FloatingActionButton] for this slot.
277 * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
278 * possible options available.
279 * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
280 * [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
281 * [floatingActionButton].
282 * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right for
283 * RTL).
284 * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
285 * @param drawerShape shape of the drawer sheet (if set)
286 * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
287 * drawer sheet (if set)
288 * @param drawerBackgroundColor background color to be used for the drawer sheet
289 * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
290 * the matching content color for [drawerBackgroundColor], or, if it is not a color from the
291 * theme, this will keep the same value set above this Surface.
292 * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
293 * @param backgroundColor background of the scaffold body
294 * @param contentColor color of the content in scaffold body. Defaults to either the matching
295 * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
296 * the same value set above this Surface.
297 * @param content content of your screen. The lambda receives an [PaddingValues] that should be
298 * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
299 * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
300 * the child of the scroll, and not on the scroll itself.
301 */
302 @Composable
Scaffoldnull303 fun Scaffold(
304 modifier: Modifier = Modifier,
305 scaffoldState: ScaffoldState = rememberScaffoldState(),
306 topBar: @Composable () -> Unit = {},
307 bottomBar: @Composable () -> Unit = {},
308 snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
309 floatingActionButton: @Composable () -> Unit = {},
310 floatingActionButtonPosition: FabPosition = FabPosition.End,
311 isFloatingActionButtonDocked: Boolean = false,
312 drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
313 drawerGesturesEnabled: Boolean = true,
314 drawerShape: Shape = MaterialTheme.shapes.large,
315 drawerElevation: Dp = DrawerDefaults.Elevation,
316 drawerBackgroundColor: Color = MaterialTheme.colors.surface,
317 drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
318 drawerScrimColor: Color = DrawerDefaults.scrimColor,
319 backgroundColor: Color = MaterialTheme.colors.background,
320 contentColor: Color = contentColorFor(backgroundColor),
321 content: @Composable (PaddingValues) -> Unit
322 ) {
323 Scaffold(
324 WindowInsets(0.dp),
325 modifier,
326 scaffoldState,
327 topBar,
328 bottomBar,
329 snackbarHost,
330 floatingActionButton,
331 floatingActionButtonPosition,
332 isFloatingActionButtonDocked,
333 drawerContent,
334 drawerGesturesEnabled,
335 drawerShape,
336 drawerElevation,
337 drawerBackgroundColor,
338 drawerContentColor,
339 drawerScrimColor,
340 backgroundColor,
341 contentColor,
342 content
343 )
344 }
345
346 /** Object containing various default values for [Scaffold] component. */
347 object ScaffoldDefaults {
348 /** Recommended insets to be used and consumed by the scaffold content slot */
349 val contentWindowInsets: WindowInsets
350 @Composable get() = WindowInsets.systemBarsForVisualComponents
351 }
352
353 /**
354 * Layout for a [Scaffold]'s content.
355 *
356 * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
357 * @param fabPosition [FabPosition] for the FAB (if present)
358 * @param topBar the content to place at the top of the [Scaffold], typically a [TopAppBar]
359 * @param content the main 'body' of the [Scaffold]
360 * @param snackbar the [Snackbar] displayed on top of the [content]
361 * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar] and
362 * above the [bottomBar]
363 * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the [content],
364 * typically a [BottomAppBar].
365 */
366 @Composable
367 @UiComposable
ScaffoldLayoutnull368 private fun ScaffoldLayout(
369 isFabDocked: Boolean,
370 fabPosition: FabPosition,
371 topBar: @Composable @UiComposable () -> Unit,
372 content: @Composable @UiComposable (PaddingValues) -> Unit,
373 snackbar: @Composable @UiComposable () -> Unit,
374 fab: @Composable @UiComposable () -> Unit,
375 contentWindowInsets: WindowInsets,
376 bottomBar: @Composable @UiComposable () -> Unit
377 ) {
378 // Create the backing value for the content padding
379 // These values will be updated during measurement, but before subcomposing the body content
380 // Remembering and updating a single PaddingValues avoids needing to recompose when the values
381 // change
382 val contentPadding = remember {
383 object : PaddingValues {
384 var paddingHolder by mutableStateOf(PaddingValues(0.dp))
385
386 override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
387 paddingHolder.calculateLeftPadding(layoutDirection)
388
389 override fun calculateTopPadding(): Dp = paddingHolder.calculateTopPadding()
390
391 override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
392 paddingHolder.calculateRightPadding(layoutDirection)
393
394 override fun calculateBottomPadding(): Dp = paddingHolder.calculateBottomPadding()
395 }
396 }
397
398 SubcomposeLayout { constraints ->
399 val layoutWidth = constraints.maxWidth
400 val layoutHeight = constraints.maxHeight
401
402 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
403
404 val topBarPlaceables =
405 subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
406 it.measure(looseConstraints)
407 }
408
409 val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
410
411 val snackbarPlaceables =
412 subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
413 // respect only bottom and horizontal for snackbar and fab
414 val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
415 val rightInset =
416 contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
417 val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
418 // offset the snackbar constraints by the insets values
419 it.measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset))
420 }
421
422 val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
423
424 val fabPlaceables =
425 subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
426 // respect only bottom and horizontal for snackbar and fab
427 val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
428 val rightInset =
429 contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
430 val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
431 measurable.measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset))
432 }
433
434 val fabPlacement =
435 if (fabPlaceables.isNotEmpty()) {
436 val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
437 val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
438 // FAB distance from the left of the layout, taking into account LTR / RTL
439 if (fabWidth != 0 && fabHeight != 0) {
440 val fabLeftOffset =
441 when (fabPosition) {
442 FabPosition.Start -> {
443 if (layoutDirection == LayoutDirection.Ltr) {
444 FabSpacing.roundToPx()
445 } else {
446 layoutWidth - FabSpacing.roundToPx() - fabWidth
447 }
448 }
449 FabPosition.End -> {
450 if (layoutDirection == LayoutDirection.Ltr) {
451 layoutWidth - FabSpacing.roundToPx() - fabWidth
452 } else {
453 FabSpacing.roundToPx()
454 }
455 }
456 else -> (layoutWidth - fabWidth) / 2
457 }
458
459 FabPlacement(
460 isDocked = isFabDocked,
461 left = fabLeftOffset,
462 width = fabWidth,
463 height = fabHeight
464 )
465 } else {
466 null
467 }
468 } else {
469 null
470 }
471
472 val bottomBarPlaceables =
473 subcompose(ScaffoldLayoutContent.BottomBar) {
474 CompositionLocalProvider(
475 LocalFabPlacement provides fabPlacement,
476 content = bottomBar
477 )
478 }
479 .fastMap { it.measure(looseConstraints) }
480
481 val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
482 val fabOffsetFromBottom =
483 fabPlacement?.let {
484 if (bottomBarHeight == null) {
485 it.height +
486 FabSpacing.roundToPx() +
487 contentWindowInsets.getBottom(this@SubcomposeLayout)
488 } else {
489 if (isFabDocked) {
490 // Total height is the bottom bar height + half the FAB height
491 bottomBarHeight + (it.height / 2)
492 } else {
493 // Total height is the bottom bar height + the FAB height + the padding
494 // between the FAB and bottom bar
495 bottomBarHeight + it.height + FabSpacing.roundToPx()
496 }
497 }
498 }
499
500 val snackbarOffsetFromBottom =
501 if (snackbarHeight != 0) {
502 snackbarHeight +
503 (fabOffsetFromBottom
504 ?: bottomBarHeight
505 ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
506 } else {
507 0
508 }
509
510 // Update the backing state for the content padding before subcomposing the body
511 val insets = contentWindowInsets.asPaddingValues(this)
512 contentPadding.paddingHolder =
513 PaddingValues(
514 top =
515 if (topBarPlaceables.isEmpty()) {
516 insets.calculateTopPadding()
517 } else {
518 0.dp
519 },
520 bottom =
521 if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
522 insets.calculateBottomPadding()
523 } else {
524 bottomBarHeight.toDp()
525 },
526 start = insets.calculateStartPadding(layoutDirection),
527 end = insets.calculateEndPadding(layoutDirection)
528 )
529
530 val bodyContentHeight = layoutHeight - topBarHeight
531
532 val bodyContentPlaceables =
533 subcompose(ScaffoldLayoutContent.MainContent) { content(contentPadding) }
534 .fastMap { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }
535
536 layout(layoutWidth, layoutHeight) {
537 // Placing to control drawing order to match default elevation of each placeable
538
539 bodyContentPlaceables.fastForEach { it.place(0, topBarHeight) }
540 topBarPlaceables.fastForEach { it.place(0, 0) }
541 snackbarPlaceables.fastForEach { it.place(0, layoutHeight - snackbarOffsetFromBottom) }
542 // The bottom bar is always at the bottom of the layout
543 bottomBarPlaceables.fastForEach { it.place(0, layoutHeight - (bottomBarHeight ?: 0)) }
544 // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
545 fabPlaceables.fastForEach {
546 it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
547 }
548 }
549 }
550 }
551
552 /**
553 * Placement information for a [FloatingActionButton] inside a [Scaffold].
554 *
555 * @property isDocked whether the FAB should be docked with the bottom bar
556 * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
557 * support
558 * @property width the width of the FAB
559 * @property height the height of the FAB
560 */
561 @Immutable
562 internal class FabPlacement(val isDocked: Boolean, val left: Int, val width: Int, val height: Int)
563
564 /**
565 * CompositionLocal containing a [FabPlacement] that is read by [BottomAppBar] to calculate notch
566 * location.
567 */
<lambda>null568 internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
569
570 // FAB spacing above the bottom bar / bottom of the Scaffold
571 private val FabSpacing = 16.dp
572
573 private enum class ScaffoldLayoutContent {
574 TopBar,
575 MainContent,
576 Snackbar,
577 Fab,
578 BottomBar
579 }
580