Ivan Iskandar 2023-11-16 21:02:36 +07:00 committed by GitHub
parent 9ec0f73e87
commit ea15bc782a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -14,36 +14,39 @@
* limitations under the License.
*/
@file:Suppress("KDocUnresolvedReference")
package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.MutableWindowInsets
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
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.exclude
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScaffoldDefaults
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.setValue
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.layout.Layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
@ -70,8 +73,6 @@ import kotlin.math.max
* * 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
@ -99,9 +100,7 @@ import kotlin.math.max
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
rememberTopAppBarState(),
),
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {},
startBar: @Composable () -> Unit = {},
@ -113,16 +112,9 @@ fun Scaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit,
) {
// Tachiyomi: Handle consumed window insets
val remainingWindowInsets = remember { MutableWindowInsets() }
androidx.compose.material3.Surface(
modifier = Modifier
.nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.onConsumedWindowInsetsChanged {
remainingWindowInsets.insets = contentWindowInsets.exclude(
it,
)
}
.then(modifier),
color = containerColor,
contentColor = contentColor,
@ -134,7 +126,7 @@ fun Scaffold(
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,
contentWindowInsets = remainingWindowInsets,
contentWindowInsets = contentWindowInsets,
fab = floatingActionButton,
)
}
@ -152,7 +144,6 @@ fun Scaffold(
* @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,
@ -164,7 +155,47 @@ private fun ScaffoldLayout(
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit,
) {
SubcomposeLayout { constraints ->
// Create the backing values for the content padding
// These values will be updated during measurement, but before measuring and placing
// the body content
var topContentPadding by remember { mutableStateOf(0.dp) }
var startContentPadding by remember { mutableStateOf(0.dp) }
var endContentPadding by remember { mutableStateOf(0.dp) }
var bottomContentPadding by remember { mutableStateOf(0.dp) }
val contentPadding = remember {
object : PaddingValues {
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
when (layoutDirection) {
LayoutDirection.Ltr -> startContentPadding
LayoutDirection.Rtl -> endContentPadding
}
override fun calculateTopPadding(): Dp = topContentPadding
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
when (layoutDirection) {
LayoutDirection.Ltr -> endContentPadding
LayoutDirection.Rtl -> startContentPadding
}
override fun calculateBottomPadding(): Dp = bottomContentPadding
}
}
Layout(
contents = listOf(
{ Spacer(Modifier.windowInsetsTopHeight(contentWindowInsets)) },
{ Spacer(Modifier.windowInsetsBottomHeight(contentWindowInsets)) },
{ Spacer(Modifier.windowInsetsStartWidth(contentWindowInsets)) },
{ Spacer(Modifier.windowInsetsEndWidth(contentWindowInsets)) },
startBar,
topBar,
snackbar,
fab,
bottomBar,
{ content(contentPadding) },
),
) { measurables, constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
@ -175,119 +206,117 @@ private fun ScaffoldLayout(
*/
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)
val topInsetsPlaceables = measurables[0].single()
.measure(looseConstraints)
val bottomInsetsPlaceables = measurables[1].single()
.measure(looseConstraints)
val startInsetsPlaceables = measurables[2].single()
.measure(looseConstraints)
val endInsetsPlaceables = measurables[3].single()
.measure(looseConstraints)
// 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
val startInsetsWidth = startInsetsPlaceables.width
val endInsetsWidth = endInsetsPlaceables.width
// Tachiyomi: layoutWidth after horizontal insets
val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth
val topInsetsHeight = topInsetsPlaceables.height
val bottomInsetsHeight = bottomInsetsPlaceables.height
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
it.measure(topBarConstraints)
}
// Tachiyomi: Add startBar slot for Navigation Rail
val startBarPlaceables = measurables[4]
.fastMap { it.measure(looseConstraints) }
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
it.measure(looseConstraints)
}
val topBarPlaceables = measurables[5]
.fastMap { it.measure(topBarConstraints) }
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
// Tachiyomi: Calculate insets for snackbar placement offset
val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) {
(insetLayoutWidth - snackbarWidth) / 2 + leftInset
} else {
0
}
val bottomPlaceablesConstraints = looseConstraints.offset(
-startInsetsWidth - endInsetsWidth,
-bottomInsetsHeight,
)
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
measurable.measure(looseConstraints)
}
val snackbarPlaceables = measurables[6]
.fastMap { it.measure(bottomPlaceablesConstraints) }
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 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) {
val fabPlaceables = measurables[7]
.fastMap { it.measure(bottomPlaceablesConstraints) }
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
val fabPlacement = if (fabWidth > 0 && fabHeight > 0) {
// FAB distance from the left of the layout, taking into account LTR / RTL
val fabLeftOffset = when (fabPosition) {
FabPosition.Start -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset
FabSpacing.roundToPx()
} else {
FabSpacing.roundToPx() + leftInset
layoutWidth - FabSpacing.roundToPx() - fabWidth
}
} else {
leftInset + ((insetLayoutWidth - 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
?.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()
FabPosition.End, FabPosition.EndOverlay -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
topBarHeight.toDp()
},
bottom = // Tachiyomi: Also take account of fab height when providing inner padding
if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) {
max(insets.calculateBottomPadding(), fabOffsetDp)
} else {
max(bottomBarHeightPx.toDp(), fabOffsetDp)
},
start = max(
insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
startBarWidth.toDp(),
),
end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection),
)
content(innerPadding)
}.fastMap { it.measure(looseConstraints) }
FabSpacing.roundToPx()
}
}
else -> (layoutWidth - fabWidth) / 2
}
FabPlacement(
left = fabLeftOffset,
width = fabWidth,
height = fabHeight,
)
} else {
null
}
val bottomBarPlaceables = measurables[8]
.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val fabOffsetFromBottom = fabPlacement?.let {
if (fabPosition == FabPosition.EndOverlay) {
it.height + FabSpacing.roundToPx() + bottomInsetsHeight
} else {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
max(bottomBarHeight, bottomInsetsHeight) + it.height + FabSpacing.roundToPx()
}
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + max(
fabOffsetFromBottom ?: 0,
max(
bottomBarHeight,
bottomInsetsHeight,
),
)
} else {
0
}
// Update the backing value for the content padding of the body content
// We do this before measuring or placing the body content
topContentPadding = max(topBarHeight, topInsetsHeight).toDp()
bottomContentPadding = max(fabOffsetFromBottom ?: 0, max(bottomBarHeight, bottomInsetsHeight)).toDp()
startContentPadding = max(startBarWidth, startInsetsWidth).toDp()
endContentPadding = endInsetsWidth.toDp()
val bodyContentPlaceables = measurables[9]
.fastMap { it.measure(looseConstraints) }
layout(layoutWidth, layoutHeight) {
// Inset spacers are just for convenient measurement logic, no need to place them
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.fastForEach {
it.place(0, 0)
}
@ -299,50 +328,27 @@ private fun ScaffoldLayout(
}
snackbarPlaceables.fastForEach {
it.place(
snackbarLeft,
(layoutWidth - snackbarWidth) / 2 + when (layoutDirection) {
LayoutDirection.Ltr -> startInsetsWidth
LayoutDirection.Rtl -> endInsetsWidth
},
layoutHeight - snackbarOffsetFromBottom,
)
}
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
it.place(0, layoutHeight - bottomBarHeight)
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlaceables.fastForEach {
it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
fabPlacement?.let { placement ->
fabPlaceables.fastForEach {
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].
*
@ -358,12 +364,5 @@ internal class FabPlacement(
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, StartBar }