123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- /*
- * 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 eu.kanade.presentation.components
- import androidx.compose.foundation.layout.PaddingValues
- import androidx.compose.foundation.layout.WindowInsets
- import androidx.compose.foundation.layout.asPaddingValues
- import androidx.compose.foundation.layout.safeDrawing
- import androidx.compose.material3.ExperimentalMaterial3Api
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.TopAppBarDefaults
- import androidx.compose.material3.TopAppBarScrollBehavior
- import androidx.compose.material3.contentColorFor
- import androidx.compose.material3.rememberTopAppBarState
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.CompositionLocalProvider
- import androidx.compose.runtime.Immutable
- import androidx.compose.runtime.staticCompositionLocalOf
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.graphics.Color
- import androidx.compose.ui.input.nestedscroll.nestedScroll
- import androidx.compose.ui.layout.SubcomposeLayout
- import androidx.compose.ui.unit.Constraints
- import androidx.compose.ui.unit.LayoutDirection
- import androidx.compose.ui.unit.dp
- /**
- * <a href="https://material.io/design/layout/understanding-layout.html" class="external" target="_blank">Material Design layout</a>.
- *
- * 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
- *
- * Tachiyomi changes:
- * * Remove height constraint for expanded app bar
- * * Also take account of fab height when providing inner padding
- *
- * @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 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.
- */
- @ExperimentalMaterial3Api
- @Composable
- fun Scaffold(
- modifier: Modifier = Modifier,
- topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
- bottomBar: @Composable () -> Unit = {},
- snackbarHost: @Composable () -> Unit = {},
- floatingActionButton: @Composable () -> Unit = {},
- floatingActionButtonPosition: FabPosition = FabPosition.End,
- containerColor: Color = MaterialTheme.colorScheme.background,
- contentColor: Color = contentColorFor(containerColor),
- content: @Composable (PaddingValues) -> Unit,
- ) {
- /**
- * Tachiyomi: Pass scroll behavior to topBar
- */
- val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
- androidx.compose.material3.Surface(
- modifier = Modifier
- .nestedScroll(scrollBehavior.nestedScrollConnection)
- .then(modifier),
- color = containerColor,
- contentColor = contentColor,
- ) {
- ScaffoldLayout(
- fabPosition = floatingActionButtonPosition,
- topBar = { topBar(scrollBehavior) },
- bottomBar = bottomBar,
- content = content,
- snackbar = snackbarHost,
- 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,
- bottomBar: @Composable () -> Unit,
- ) {
- SubcomposeLayout { constraints ->
- val layoutWidth = constraints.maxWidth
- val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
- /**
- * Tachiyomi: Remove height constraint for expanded app bar
- */
- val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
- layout(layoutWidth, layoutHeight) {
- val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map {
- it.measure(topBarConstraints)
- }
- val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
- val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
- it.measure(looseConstraints)
- }
- 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).takeIf { it.height != 0 && it.width != 0 }
- }
- val fabHeight = fabPlaceables.maxByOrNull { it.height }?.height ?: 0
- val fabPlacement = if (fabPlaceables.isNotEmpty()) {
- val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
- // 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 ?: 0
- val fabOffsetFromBottom = fabPlacement?.let {
- if (bottomBarHeight == 0) {
- it.height + FabSpacing.roundToPx()
- } 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)
- } else {
- 0
- }
- /**
- * Tachiyomi: Also take account of fab height when providing inner padding
- */
- val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
- val insets = WindowInsets.Companion.safeDrawing
- .asPaddingValues(this@SubcomposeLayout)
- val innerPadding = PaddingValues(
- top =
- if (topBarHeight == 0) insets.calculateTopPadding()
- else topBarHeight.toDp(),
- bottom =
- (
- if (bottomBarHeight == 0) insets.calculateBottomPadding()
- else bottomBarHeight.toDp()
- ) + fabHeight.toDp(),
- start = insets.calculateLeftPadding((this@SubcomposeLayout).layoutDirection),
- end = insets.calculateRightPadding((this@SubcomposeLayout).layoutDirection),
- )
- content(innerPadding)
- }.map { it.measure(looseConstraints) }
- // Placing to control drawing order to match default elevation of each placeable
- bodyContentPlaceables.forEach {
- it.place(0, 0)
- }
- topBarPlaceables.forEach {
- it.place(0, 0)
- }
- snackbarPlaceables.forEach {
- it.place(
- (layoutWidth - snackbarWidth) / 2,
- layoutHeight - snackbarOffsetFromBottom,
- )
- }
- // The bottom bar is always at the bottom of the layout
- bottomBarPlaceables.forEach {
- it.place(0, layoutHeight - bottomBarHeight)
- }
- // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
- fabPlacement?.let { placement ->
- fabPlaceables.forEach {
- it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
- }
- }
- }
- }
- }
- /**
- * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
- */
- @ExperimentalMaterial3Api
- @JvmInline
- value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
- companion object {
- /**
- * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it
- * exists)
- */
- val Center = FabPosition(0)
- /**
- * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it
- * exists)
- */
- val End = FabPosition(1)
- }
- override fun toString(): String {
- return when (this) {
- Center -> "FabPosition.Center"
- else -> "FabPosition.End"
- }
- }
- }
- /**
- * 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<FabPlacement?> { 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 }
|