123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- 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
- @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,
- ) {
-
- 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,
- )
- }
- }
- @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)
-
- 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
-
- 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 {
-
-
- bottomBarHeight + it.height + FabSpacing.roundToPx()
- }
- }
- val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
- snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
- } else {
- 0
- }
-
- 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) }
-
- bodyContentPlaceables.forEach {
- it.place(0, 0)
- }
- topBarPlaceables.forEach {
- it.place(0, 0)
- }
- snackbarPlaceables.forEach {
- it.place(
- (layoutWidth - snackbarWidth) / 2,
- layoutHeight - snackbarOffsetFromBottom,
- )
- }
-
- bottomBarPlaceables.forEach {
- it.place(0, layoutHeight - bottomBarHeight)
- }
-
- fabPlacement?.let { placement ->
- fabPlaceables.forEach {
- it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
- }
- }
- }
- }
- }
- @ExperimentalMaterial3Api
- @JvmInline
- value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
- companion object {
-
- val Center = FabPosition(0)
-
- val End = FabPosition(1)
- }
- override fun toString(): String {
- return when (this) {
- Center -> "FabPosition.Center"
- else -> "FabPosition.End"
- }
- }
- }
- @Immutable
- internal class FabPlacement(
- val left: Int,
- val width: Int,
- val height: Int,
- )
- internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
- private val FabSpacing = 16.dp
- private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
|