Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Commit

Permalink
Simplify NestedScaffold (#1684)
Browse files Browse the repository at this point in the history
We now use a custom WindowInsets instance to pass down the nested
padding.
  • Loading branch information
chrisbanes authored Dec 24, 2023
1 parent fe93a82 commit 382279f
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 245 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import app.cash.paging.LoadStateLoading
import app.cash.paging.compose.LazyPagingItems
Expand Down Expand Up @@ -128,8 +129,10 @@ fun <E : Entry> EntryGrid(

LazyVerticalGrid(
columns = GridCells.Fixed((columns / 1.5).roundToInt()),
contentPadding = paddingValues +
contentPadding = paddingValues.plus(
PaddingValues(horizontal = bodyMargin, vertical = gutter),
LocalLayoutDirection.current,
),
horizontalArrangement = Arrangement.spacedBy(gutter),
verticalArrangement = Arrangement.spacedBy(gutter),
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,26 @@ fun HazeScaffold(
NestedScaffold(
modifier = modifier,
topBar = {
Box(
modifier = Modifier.thenIf(blurTopBar) { hazeChild(hazeState) },
content = { topBar() },
)
if (blurTopBar) {
// We explicitly only want to add a Box if we are blurring.
// Scaffold has logic which changes based on whether `bottomBar` contains a layout node.
Box(Modifier.hazeChild(hazeState)) {
topBar()
}
} else {
topBar()
}
},
bottomBar = {
Box(
modifier = Modifier.thenIf(blurBottomBar) { hazeChild(hazeState) },
content = { bottomBar() },
)
if (blurBottomBar) {
// We explicitly only want to add a Box if we are blurring.
// Scaffold has logic which changes based on whether `bottomBar` contains a layout node.
Box(Modifier.hazeChild(hazeState)) {
bottomBar()
}
} else {
bottomBar()
}
},
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
Expand All @@ -61,7 +71,8 @@ fun HazeScaffold(
state = hazeState,
backgroundColor = containerColor,
),
content = { content(contentPadding) },
)
) {
content(contentPadding)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@

package app.tivi.common.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.MutableWindowInsets
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
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.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import app.tivi.common.compose.ui.minus
import app.tivi.common.compose.ui.plus

private val LocalScaffoldContentPadding = staticCompositionLocalOf { PaddingValues(0.dp) }
Expand All @@ -37,6 +37,7 @@ private val LocalScaffoldContentPadding = staticCompositionLocalOf { PaddingValu
*
* - Supports being used nested. The `contentPadding` is compounded on each level.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun NestedScaffold(
modifier: Modifier = Modifier,
Expand All @@ -50,194 +51,56 @@ internal fun NestedScaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit,
) {
Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
NestedScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = topBar,
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,
contentWindowInsets = contentWindowInsets,
fab = floatingActionButton,
incomingContentPadding = LocalScaffoldContentPadding.current,
val upstreamContentPadding = LocalScaffoldContentPadding.current
val layoutDirection = LocalLayoutDirection.current

val insets = remember {
MutableWindowInsets(
contentWindowInsets.add(PaddingValuesInsets(upstreamContentPadding)),
)
}
}

/**
* 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].
*/
@Composable
private fun NestedScaffoldLayout(
fabPosition: FabPosition,
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
incomingContentPadding: PaddingValues,
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).map {
it.measure(looseConstraints)
}

val contentInsets = incomingContentPadding + contentWindowInsets
.asPaddingValues(this@SubcomposeLayout)
val leftInset = contentInsets.calculateLeftPadding(layoutDirection).roundToPx()
val rightInset = contentInsets.calculateRightPadding(layoutDirection).roundToPx()
val bottomInset = contentInsets.calculateBottomPadding().roundToPx()

val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0

val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
// respect only bottom and horizontal for snackbar and fab
// offset the snackbar constraints by the insets values
it.measure(
looseConstraints.offset(-leftInset - rightInset, -bottomInset),
)
}

val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0

val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
measurable.measure(
looseConstraints.offset(-leftInset - rightInset, -bottomInset),
).takeIf { it.height != 0 && it.width != 0 }
}

val fabPlacement = if (fabPlaceables.isNotEmpty()) {
val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
val fabHeight = fabPlaceables.maxByOrNull { it.height }!!.height
// FAB distance from the left of the layout, taking into account LTR / RTL
val fabLeftOffset = if (fabPosition == 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,
)
}.map { it.measure(looseConstraints) }

val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
(bottomBarHeight ?: bottomInset) + it.height + FabSpacing.roundToPx()
}

val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight ?: bottomInset)
} else {
0
}

val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val innerPadding = PaddingValues(
top = if (topBarPlaceables.isEmpty()) {
contentInsets.calculateTopPadding()
} else {
topBarHeight.toDp()
},
bottom = if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
contentInsets.calculateBottomPadding()
} else {
bottomBarHeight.toDp()
},
start = contentInsets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
end = contentInsets.calculateEndPadding((this@SubcomposeLayout).layoutDirection),
)

// Scaffold always applies the insets, so we only want to pass down the content padding
// without the insets (i.e. padding from the bottom bar, etc)
CompositionLocalProvider(LocalScaffoldContentPadding provides innerPadding) {
content(innerPadding)
}
}.map { it.measure(looseConstraints) }
LaunchedEffect(contentWindowInsets, upstreamContentPadding, layoutDirection) {
insets.insets = contentWindowInsets.add(PaddingValuesInsets(upstreamContentPadding))
}

// Placing to control drawing order to match default elevation of each placeable
Scaffold(
modifier = modifier,
topBar = topBar,
bottomBar = bottomBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
containerColor = containerColor,
contentColor = contentColor,
contentWindowInsets = insets,
) { contentPadding ->
val contentPaddingMinusInsets = contentPadding.minus(
contentWindowInsets.asPaddingValues(),
layoutDirection,
)

bodyContentPlaceables.forEach {
it.place(0, 0)
}
topBarPlaceables.forEach {
it.place(0, 0)
}
snackbarPlaceables.forEach {
it.place(
x = (layoutWidth - snackbarWidth) / 2 + leftInset,
y = layoutHeight - snackbarOffsetFromBottom,
)
}
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.forEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlacement?.let { placement ->
fabPlaceables.forEach {
it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
}
}
CompositionLocalProvider(LocalScaffoldContentPadding provides contentPaddingMinusInsets) {
content(contentPadding)
}
}
}

/**
* 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,
)
@Stable
private data class PaddingValuesInsets(private val paddingValues: PaddingValues) : WindowInsets {
override fun getLeft(density: Density, layoutDirection: LayoutDirection) = with(density) {
paddingValues.calculateLeftPadding(layoutDirection).roundToPx()
}

/**
* CompositionLocal containing a [FabPlacement] that is used to calculate the FAB bottom offset.
*/
internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
override fun getTop(density: Density) = with(density) {
paddingValues.calculateTopPadding().roundToPx()
}

// FAB spacing above the bottom bar / bottom of the Scaffold
private val FabSpacing = 16.dp
override fun getRight(density: Density, layoutDirection: LayoutDirection) = with(density) {
paddingValues.calculateRightPadding(layoutDirection).roundToPx()
}

private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
override fun getBottom(density: Density) = with(density) {
paddingValues.calculateBottomPadding().roundToPx()
}
}
Loading

0 comments on commit 382279f

Please sign in to comment.