/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapNotNull
import androidx.compose.ui.util.fastMaxBy
/**
* Material Design layout.
*
* Scaffold implements the basic material design visual layout structure.
*
* This component provides API to put together several material components to construct your
* screen, by ensuring proper layout strategy for them and collecting necessary data so these
* components will work together correctly.
*
* Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]:
*
* @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
*
* To show a [Snackbar], use [SnackbarHostState.showSnackbar].
*
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
*
* @param modifier the [Modifier] to be applied to this scaffold
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
* @param bottomBar bottom bar of the screen, typically a [NavigationBar]
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
* @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton]
* @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition].
* @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
* to have no color.
* @param contentColor the preferred color for content inside this scaffold. Defaults to either the
* matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues]
* params. Scaffold will take the insets into account from the top/bottom only if the [topBar]/
* [bottomBar] are not present, as the scaffold expect [topBar]/[bottomBar] to handle insets
* instead. Any insets consumed by other insets padding modifiers or [consumeWindowInsets] on a
* parent layout will be excluded from [contentWindowInsets].
* @param content content of the screen. The lambda receives a [PaddingValues] that should be
* applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
* the child of the scroll, and not on the scroll itself.
*/
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
) {
val safeInsets = remember(contentWindowInsets) {
MutableWindowInsets(contentWindowInsets)
}
Surface(
modifier = modifier.onConsumedWindowInsetsChanged { consumedWindowInsets ->
// Exclude currently consumed window insets from user provided contentWindowInsets
safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets)
},
color = containerColor,
contentColor = contentColor) {
ScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = topBar,
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,
contentWindowInsets = safeInsets,
fab = floatingActionButton
)
}
}
/**
* Layout for a [Scaffold]'s content.
*
* @param fabPosition [FabPosition] for the FAB (if present)
* @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar]
* @param content the main 'body' of the [Scaffold]
* @param snackbar the [Snackbar] displayed on top of the [content]
* @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
* and above the [bottomBar]
* @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
* [content], typically a [NavigationBar].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ScaffoldLayout(
fabPosition: FabPosition,
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit
) {
if (ScaffoldSubcomposeInMeasureFix) {
ScaffoldLayoutWithMeasureFix(
fabPosition = fabPosition,
topBar = topBar,
content = content,
snackbar = snackbar,
fab = fab,
contentWindowInsets = contentWindowInsets,
bottomBar = bottomBar
)
} else {
LegacyScaffoldLayout(
fabPosition = fabPosition,
topBar = topBar,
content = content,
snackbar = snackbar,
fab = fab,
contentWindowInsets = contentWindowInsets,
bottomBar = bottomBar
)
}
}
/**
* Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
*/
@Composable
private fun ScaffoldLayoutWithMeasureFix(
fabPosition: FabPosition,
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit
) {
SubcomposeLayout { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
it.measure(looseConstraints)
}
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
// respect only bottom and horizontal for snackbar and fab
val leftInset = contentWindowInsets
.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset = contentWindowInsets
.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
// offset the snackbar constraints by the insets values
it.measure(
looseConstraints.offset(
-leftInset - rightInset,
-bottomInset
)
)
}
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable ->
// respect only bottom and horizontal for snackbar and fab
val leftInset =
contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset =
contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
measurable.measure(
looseConstraints.offset(
-leftInset - rightInset,
-bottomInset
)
)
.takeIf { it.height != 0 && it.width != 0 }
}
val fabPlacement = if (fabPlaceables.isNotEmpty()) {
val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
// FAB distance from the left of the layout, taking into account LTR / RTL
val fabLeftOffset = when (fabPosition) {
FabPosition.Start -> {
if (layoutDirection == LayoutDirection.Ltr) {
FabSpacing.roundToPx()
} else {
layoutWidth - FabSpacing.roundToPx() - fabWidth
}
}
FabPosition.End, FabPosition.EndOverlay -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
FabSpacing.roundToPx()
}
}
else -> (layoutWidth - fabWidth) / 2
}
FabPlacement(
left = fabLeftOffset,
width = fabWidth,
height = fabHeight
)
} else {
null
}
val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
CompositionLocalProvider(
LocalFabPlacement provides fabPlacement,
content = bottomBar
)
}.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
if (bottomBarHeight == null || fabPosition == FabPosition.EndOverlay) {
it.height + FabSpacing.roundToPx() +
contentWindowInsets.getBottom(this@SubcomposeLayout)
} else {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
bottomBarHeight + it.height + FabSpacing.roundToPx()
}
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight +
(fabOffsetFromBottom ?: bottomBarHeight
?: contentWindowInsets.getBottom(this@SubcomposeLayout))
} else {
0
}
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
val innerPadding = PaddingValues(
top =
if (topBarPlaceables.isEmpty()) {
insets.calculateTopPadding()
} else {
topBarHeight.toDp()
},
bottom =
if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
insets.calculateBottomPadding()
} else {
bottomBarHeight.toDp()
},
start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
)
content(innerPadding)
}.fastMap { it.measure(looseConstraints) }
layout(layoutWidth, layoutHeight) {
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.fastForEach {
it.place(0, 0)
}
topBarPlaceables.fastForEach {
it.place(0, 0)
}
snackbarPlaceables.fastForEach {
it.place(
(layoutWidth - snackbarWidth) / 2 +
contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection),
layoutHeight - snackbarOffsetFromBottom
)
}
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlacement?.let { placement ->
fabPlaceables.fastForEach {
it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
}
}
}
}
}
/**
* Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
*/
@Composable
private fun LegacyScaffoldLayout(
fabPosition: FabPosition,
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit
) {
SubcomposeLayout { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
layout(layoutWidth, layoutHeight) {
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
it.measure(looseConstraints)
}
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
// respect only bottom and horizontal for snackbar and fab
val leftInset = contentWindowInsets
.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset = contentWindowInsets
.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
// offset the snackbar constraints by the insets values
it.measure(
looseConstraints.offset(
-leftInset - rightInset,
-bottomInset
)
)
}
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable ->
// respect only bottom and horizontal for snackbar and fab
val leftInset =
contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset =
contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
measurable.measure(
looseConstraints.offset(
-leftInset - rightInset,
-bottomInset
)
)
.takeIf { it.height != 0 && it.width != 0 }
}
val fabPlacement = if (fabPlaceables.isNotEmpty()) {
val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
// FAB distance from the left of the layout, taking into account LTR / RTL
val fabLeftOffset = when (fabPosition) {
FabPosition.Start -> {
if (layoutDirection == LayoutDirection.Ltr) {
FabSpacing.roundToPx()
} else {
layoutWidth - FabSpacing.roundToPx() - fabWidth
}
}
FabPosition.End -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
FabSpacing.roundToPx()
}
}
else -> (layoutWidth - fabWidth) / 2
}
FabPlacement(
left = fabLeftOffset,
width = fabWidth,
height = fabHeight
)
} else {
null
}
val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
CompositionLocalProvider(
LocalFabPlacement provides fabPlacement,
content = bottomBar
)
}.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
if (bottomBarHeight == null) {
it.height + FabSpacing.roundToPx() +
contentWindowInsets.getBottom(this@SubcomposeLayout)
} else {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
bottomBarHeight + it.height + FabSpacing.roundToPx()
}
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight +
(fabOffsetFromBottom ?: bottomBarHeight
?: contentWindowInsets.getBottom(this@SubcomposeLayout))
} else {
0
}
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
val innerPadding = PaddingValues(
top =
if (topBarPlaceables.isEmpty()) {
insets.calculateTopPadding()
} else {
topBarHeight.toDp()
},
bottom =
if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
insets.calculateBottomPadding()
} else {
bottomBarHeight.toDp()
},
start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
)
content(innerPadding)
}.fastMap { it.measure(looseConstraints) }
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.fastForEach {
it.place(0, 0)
}
topBarPlaceables.fastForEach {
it.place(0, 0)
}
snackbarPlaceables.fastForEach {
it.place(
(layoutWidth - snackbarWidth) / 2 +
contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection),
layoutHeight - snackbarOffsetFromBottom
)
}
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlacement?.let { placement ->
fabPlaceables.fastForEach {
it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
}
}
}
}
}
/**
* Object containing various default values for [Scaffold] component.
*/
object ScaffoldDefaults {
/**
* Default insets to be used and consumed by the scaffold content slot
*/
val contentWindowInsets: WindowInsets
@Composable
get() = WindowInsets.systemBarsForVisualComponents
}
/**
* The possible positions for a [FloatingActionButton] attached to a [Scaffold].
*/
@kotlin.jvm.JvmInline
value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
companion object {
/**
* Position FAB at the bottom of the screen at the start, above the [NavigationBar] (if it
* exists)
*/
val Start = FabPosition(0)
/**
* Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it
* exists)
*/
val Center = FabPosition(1)
/**
* Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it
* exists)
*/
val End = FabPosition(2)
/**
* Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if
* it exists)
*/
val EndOverlay = FabPosition(3)
}
override fun toString(): String {
return when (this) {
Start -> "FabPosition.Start"
Center -> "FabPosition.Center"
End -> "FabPosition.End"
else -> "FabPosition.EndOverlay"
}
}
}
/**
* Flag indicating if [Scaffold] should subcompose and measure its children during measurement or
* during placement.
* Set this flag to false to keep Scaffold's old measurement behavior (measuring in placement).
*
* This flag will be removed in Compose 1.6.0-beta01. If you encounter any issues with the
* new behavior, please file an issue at: issuetracker.google.com/issues/new?component=742043
*/
// TODO(b/299621062): Remove flag before beta
@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
@get:Suppress("GetterSetterNames")
@get:ExperimentalMaterial3Api
@set:ExperimentalMaterial3Api
@ExperimentalMaterial3Api
var ScaffoldSubcomposeInMeasureFix by mutableStateOf(true)
/**
* Placement information for a [FloatingActionButton] inside a [Scaffold].
*
* @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
* support
* @property width the width of the FAB
* @property height the height of the FAB
*/
@Immutable
internal class FabPlacement(
val left: Int,
val width: Int,
val height: Int
)
/**
* CompositionLocal containing a [FabPlacement] that is used to calculate the FAB bottom offset.
*/
internal val LocalFabPlacement = staticCompositionLocalOf { null }
// FAB spacing above the bottom bar / bottom of the Scaffold
private val FabSpacing = 16.dp
private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }