Use upstream scaffold (#3834)

This commit is contained in:
AntsyLich
2026-08-22 08:22:45 +06:00
committed by GitHub
parent 9ec479266b
commit a2c24ff042
6 changed files with 146 additions and 566 deletions
@@ -2,22 +2,18 @@ package eu.kanade.tachiyomi.ui.home
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItem
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -29,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -52,9 +49,6 @@ import mihon.app.di.appGraph
import soup.compose.material.motion.animation.materialFadeThroughIn import soup.compose.material.motion.animation.materialFadeThroughIn
import soup.compose.material.motion.animation.materialFadeThroughOut import soup.compose.material.motion.animation.materialFadeThroughOut
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.NavigationBar
import tachiyomi.presentation.core.components.material.NavigationRail
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
object HomeScreen : Screen() { object HomeScreen : Screen() {
@@ -86,46 +80,43 @@ object HomeScreen : Screen() {
) { tabNavigator -> ) { tabNavigator ->
// Provide usable navigator to content screen // Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) { CompositionLocalProvider(LocalNavigator provides navigator) {
Scaffold( val tabletUi = isTabletUi()
startBar = { val navigationSuiteType = if (tabletUi) {
if (isTabletUi()) { NavigationSuiteType.NavigationRail
NavigationRail { } else {
TABS.fastForEach { NavigationSuiteType.NavigationBar
NavigationRailItem(it) }
val navigationSuiteState = rememberNavigationSuiteScaffoldState()
LaunchedEffect(navigationSuiteState, tabletUi) {
if (tabletUi) navigationSuiteState.show()
showBottomNavEvent.receiveAsFlow().collectLatest { show ->
if (tabletUi || show) {
navigationSuiteState.show()
} else {
navigationSuiteState.hide()
} }
} }
} }
NavigationSuiteScaffold(
navigationSuiteType = navigationSuiteType,
state = navigationSuiteState,
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp),
),
navigationItemVerticalArrangement = Arrangement.Center,
navigationItems = {
TABS.fastForEach { NavigationSuiteItem(it, navigationSuiteType) }
}, },
bottomBar = {
if (!isTabletUi()) {
val bottomNavVisible by produceState(initialValue = true) {
showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
}
AnimatedVisibility(
visible = bottomNavVisible,
enter = expandVertically(),
exit = shrinkVertically(),
) {
NavigationBar {
TABS.fastForEach {
NavigationBarItem(it)
}
}
}
}
},
contentWindowInsets = WindowInsets(0),
) { contentPadding ->
Box(
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding),
) { ) {
AnimatedContent( AnimatedContent(
targetState = tabNavigator.current, targetState = tabNavigator.current,
transitionSpec = { transitionSpec = {
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith materialFadeThroughIn(
materialFadeThroughOut(durationMillis = TabFadeDuration) initialScale = 1f,
durationMillis = TabFadeDuration,
) togetherWith materialFadeThroughOut(durationMillis = TabFadeDuration)
}, },
label = "tabContent", label = "tabContent",
) { ) {
@@ -135,7 +126,6 @@ object HomeScreen : Screen() {
} }
} }
} }
}
val goToLibraryTab = { tabNavigator.current = LibraryTab } val goToLibraryTab = { tabNavigator.current = LibraryTab }
@@ -176,116 +166,91 @@ object HomeScreen : Screen() {
} }
@Composable @Composable
private fun RowScope.NavigationBarItem(tab: eu.kanade.presentation.util.Tab) { private fun NavigationSuiteItem(
val tabNavigator = LocalTabNavigator.current tab: eu.kanade.presentation.util.Tab,
val navigator = LocalNavigator.currentOrThrow navigationSuiteType: NavigationSuiteType,
val scope = rememberCoroutineScope()
val selected = tabNavigator.current::class == tab::class
NavigationBarItem(
selected = selected,
onClick = {
if (!selected) {
tabNavigator.current = tab
} else {
scope.launch { tab.onReselect(navigator) }
}
},
icon = { NavigationIconItem(tab) },
label = {
Text(
text = tab.options.title,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
alwaysShowLabel = true,
)
}
@Composable
fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab) {
val tabNavigator = LocalTabNavigator.current
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val selected = tabNavigator.current::class == tab::class
NavigationRailItem(
selected = selected,
onClick = {
if (!selected) {
tabNavigator.current = tab
} else {
scope.launch { tab.onReselect(navigator) }
}
},
icon = { NavigationIconItem(tab) },
label = {
Text(
text = tab.options.title,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
alwaysShowLabel = true,
)
}
@Composable
private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
val context = LocalContext.current
BadgedBox(
badge = {
when {
tab is UpdatesTab -> {
val count by produceState(initialValue = 0) {
val pref = context.appGraph.libraryPreferences
combine(
pref.newShowUpdatesCount.changes(),
pref.newUpdatesCount.changes(),
) { show, count -> if (show) count else 0 }
.collectLatest { value = it }
}
if (count > 0) {
Badge {
val desc = pluralStringResource(
MR.plurals.notification_chapters_generic,
count = count,
count,
)
Text(
text = count.toString(),
modifier = Modifier.semantics { contentDescription = desc },
)
}
}
}
BrowseTab::class.isInstance(tab) -> {
val count by produceState(initialValue = 0) {
context.appGraph.sourcePreferences.extensionUpdatesCount.changes()
.collectLatest { value = it }
}
if (count > 0) {
Badge {
val desc = pluralStringResource(
MR.plurals.update_check_notification_ext_updates,
count = count,
count,
)
Text(
text = count.toString(),
modifier = Modifier.semantics { contentDescription = desc },
)
}
}
}
}
},
) { ) {
val tabNavigator = LocalTabNavigator.current
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val selected = tabNavigator.current::class == tab::class
NavigationSuiteItem(
navigationSuiteType = navigationSuiteType,
selected = selected,
onClick = {
if (!selected) {
tabNavigator.current = tab
} else {
scope.launch { tab.onReselect(navigator) }
}
},
icon = {
Icon( Icon(
painter = tab.options.icon!!, painter = tab.options.icon!!,
contentDescription = tab.options.title, contentDescription = tab.options.title,
) )
},
label = {
Text(
text = tab.options.title,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
badge = tabBadge(tab),
)
}
@Composable
private fun tabBadge(tab: eu.kanade.presentation.util.Tab): (@Composable () -> Unit)? {
val context = LocalContext.current
val count by produceState(initialValue = 0, tab) {
val graph = context.appGraph
when (tab) {
is UpdatesTab -> {
combine(
graph.libraryPreferences.newShowUpdatesCount.changes(),
graph.libraryPreferences.newUpdatesCount.changes(),
) { show, count ->
if (show) count else 0
}
.collectLatest { value = it }
}
is BrowseTab -> {
graph.sourcePreferences.extensionUpdatesCount.changes()
.collectLatest { value = it }
}
else -> value = 0
}
}
if (count <= 0) return null
return {
Badge {
val desc = when (tab) {
is UpdatesTab -> pluralStringResource(
MR.plurals.notification_chapters_generic,
count = count,
count,
)
is BrowseTab -> pluralStringResource(
MR.plurals.update_check_notification_ext_updates,
count = count,
count,
)
else -> null
}
Text(
text = count.toString(),
modifier = Modifier.semantics {
if (desc != null) contentDescription = desc
},
)
}
} }
} }
+1
View File
@@ -93,6 +93,7 @@ androidx-compose-animationGraphics = { module = "androidx.compose.animation:anim
androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3NavSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" }
androidx-compose-materialIcons = { module = "androidx.compose.material:material-icons-extended" } androidx-compose-materialIcons = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtimeAnnotation = { module = "androidx.compose.runtime:runtime-annotation" } androidx-compose-runtimeAnnotation = { module = "androidx.compose.runtime:runtime-annotation" }
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
api(libs.androidx.compose.material3NavSuite)
implementation(libs.androidx.compose.materialIcons) implementation(libs.androidx.compose.materialIcons)
implementation(libs.androidx.compose.animation) implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.animationGraphics) implementation(libs.androidx.compose.animationGraphics)
@@ -1,48 +0,0 @@
package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* M3 Navbar with no horizontal spacer
*
* @see [androidx.compose.material3.NavigationBar]
*/
@Composable
fun NavigationBar(
modifier: Modifier = Modifier,
containerColor: Color = NavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit,
) {
androidx.compose.material3.Surface(
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(windowInsets)
.height(80.dp)
.selectableGroup(),
content = content,
)
}
}
@@ -1,63 +0,0 @@
package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRailDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
/**
* Center-aligned M3 Navigation rail
*
* @see [androidx.compose.material3.NavigationRail]
*/
@Composable
fun NavigationRail(
modifier: Modifier = Modifier,
containerColor: Color = NavigationRailDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
header: @Composable (ColumnScope.() -> Unit)? = null,
windowInsets: WindowInsets = NavigationRailDefaults.windowInsets,
content: @Composable ColumnScope.() -> Unit,
) {
androidx.compose.material3.Surface(
color = containerColor,
contentColor = contentColor,
modifier = modifier,
tonalElevation = 3.dp,
) {
Column(
Modifier
.fillMaxHeight()
.windowInsetsPadding(windowInsets)
.widthIn(min = 80.dp)
.padding(vertical = MaterialTheme.padding.extraSmall)
.selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
MaterialTheme.padding.extraSmall,
alignment = Alignment.CenterVertically,
),
) {
if (header != null) {
header()
Spacer(Modifier.height(MaterialTheme.padding.small))
}
content()
}
}
}
@@ -1,32 +1,9 @@
/*
* 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.
*/
@file:Suppress("KDocUnresolvedReference")
package tachiyomi.presentation.core.components.material package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.MutableWindowInsets import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.material3.FabPosition
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@@ -34,68 +11,19 @@ import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
import kotlin.math.max
/**
* <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:
* * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding
* * Fixes for fab and snackbar horizontal placements when [contentWindowInsets] is used
* * Handle consumed window insets
* * Add startBar slot for Navigation Rail
*
* @param modifier the [Modifier] to be applied to this scaffold
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
* @param startBar side bar on the start of the screen, typically a [NavigationRail]
* @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
* @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 @Composable
fun Scaffold( fun Scaffold(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -104,7 +32,6 @@ fun Scaffold(
), ),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {},
startBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End, floatingActionButtonPosition: FabPosition = FabPosition.End,
@@ -113,248 +40,45 @@ fun Scaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
// Tachiyomi: Handle consumed window insets var fabHeight by remember { mutableIntStateOf(0) }
val remainingWindowInsets = remember { MutableWindowInsets() } val density = LocalDensity.current
androidx.compose.material3.Surface(
modifier = Modifier androidx.compose.material3.Scaffold(
.nestedScroll(topBarScrollBehavior.nestedScrollConnection) modifier = modifier.nestedScroll(topBarScrollBehavior.nestedScrollConnection),
.onConsumedWindowInsetsChanged {
remainingWindowInsets.insets = contentWindowInsets.exclude(
it,
)
}
.then(modifier),
color = containerColor,
contentColor = contentColor,
) {
ScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = { topBar(topBarScrollBehavior) }, topBar = { topBar(topBarScrollBehavior) },
startBar = startBar,
bottomBar = bottomBar, bottomBar = bottomBar,
content = content, snackbarHost = snackbarHost,
snackbar = snackbarHost, floatingActionButton = {
contentWindowInsets = remainingWindowInsets, Box(Modifier.onSizeChanged { fabHeight = it.height }) {
fab = floatingActionButton, 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,
startBar: @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)
/**
* Tachiyomi: Remove height constraint for expanded app bar
*/
val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
layout(layoutWidth, layoutHeight) {
val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
// Tachiyomi: Add startBar slot for Navigation Rail
val startBarPlaceables = subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap {
it.measure(looseConstraints)
}
val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0
// Tachiyomi: layoutWidth after horizontal insets
val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
it.measure(topBarConstraints)
}
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
it.measure(looseConstraints)
}
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
// Tachiyomi: Calculate insets for snackbar placement offset
val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) {
(insetLayoutWidth - snackbarWidth) / 2 + leftInset
} else {
0
}
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
measurable.measure(looseConstraints)
}
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) {
// FAB distance from the left of the layout, taking into account LTR / RTL
// Tachiyomi: Calculate insets for fab placement offset
val fabLeftOffset = if (fabPosition == FabPosition.End) {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset
} else {
FabSpacing.roundToPx() + leftInset
}
} else {
leftInset + ((insetLayoutWidth - fabWidth) / 2)
}
FabPlacement(
left = fabLeftOffset,
width = fabWidth,
height = fabHeight,
)
} else {
null
}
val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
bottomBar()
}.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables
.fastMaxBy { it.height }
?.height
?.takeIf { it != 0 }
val fabOffsetFromBottom = fabPlacement?.let {
max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx()
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset))
} else {
0
}
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp
val bottomBarHeightPx = bottomBarHeight ?: 0
val innerPadding = PaddingValues(
top = if (topBarPlaceables.isEmpty()) {
insets.calculateTopPadding()
} else {
topBarHeight.toDp()
}, },
// Tachiyomi: Also take account of fab height when providing inner padding floatingActionButtonPosition = floatingActionButtonPosition,
bottom = if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) { containerColor = containerColor,
max(insets.calculateBottomPadding(), fabOffsetDp) contentColor = contentColor,
} else { contentWindowInsets = contentWindowInsets,
max(bottomBarHeightPx.toDp(), fabOffsetDp) ) { contentPadding ->
}, val paddingValues = remember(contentPadding) {
start = max( FabAwarePaddingValues(contentPadding) {
insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), if (fabHeight == 0) 0.dp else with(density) { fabHeight.toDp() } + FabSpacing
startBarWidth.toDp(),
),
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)
}
startBarPlaceables.fastForEach {
it.placeRelative(0, 0)
}
topBarPlaceables.fastForEach {
it.place(0, 0)
}
snackbarPlaceables.fastForEach {
it.place(
snackbarLeft,
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
fabPlaceables.fastForEach {
it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
} }
} }
content(paddingValues)
} }
} }
/** private class FabAwarePaddingValues(
* The possible positions for a [FloatingActionButton] attached to a [Scaffold]. private val base: PaddingValues,
*/ private val extraBottom: () -> Dp,
@ExperimentalMaterial3Api ) : PaddingValues {
@JvmInline override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
value class FabPosition internal constructor(private val value: Int) { base.calculateLeftPadding(layoutDirection)
companion object {
/**
* Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it
* exists)
*/
val Center = FabPosition(0)
/** override fun calculateTopPadding(): Dp = base.calculateTopPadding()
* 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 { override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
return when (this) { base.calculateRightPadding(layoutDirection)
Center -> "FabPosition.Center"
else -> "FabPosition.End" override fun calculateBottomPadding(): Dp = base.calculateBottomPadding() + extraBottom()
}
}
} }
/**
* 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,
)
// FAB spacing above the bottom bar / bottom of the Scaffold
private val FabSpacing = 16.dp private val FabSpacing = 16.dp
private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar, StartBar }