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. * 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.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.windowInsetsStartWidth
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
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
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf 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.Layout
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
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.unit.offset
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastMaxBy
@ -70,8 +73,6 @@ import kotlin.math.max
* * Pass scroll behavior to top bar by default * * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar * * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding * * 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 * * Add startBar slot for Navigation Rail
* *
* @param modifier the [Modifier] to be applied to this scaffold * @param modifier the [Modifier] to be applied to this scaffold
@ -99,9 +100,7 @@ import kotlin.math.max
@Composable @Composable
fun Scaffold( fun Scaffold(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
rememberTopAppBarState(),
),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {},
startBar: @Composable () -> Unit = {}, startBar: @Composable () -> Unit = {},
@ -113,16 +112,9 @@ fun Scaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
// Tachiyomi: Handle consumed window insets
val remainingWindowInsets = remember { MutableWindowInsets() }
androidx.compose.material3.Surface( androidx.compose.material3.Surface(
modifier = Modifier modifier = Modifier
.nestedScroll(topBarScrollBehavior.nestedScrollConnection) .nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.onConsumedWindowInsetsChanged {
remainingWindowInsets.insets = contentWindowInsets.exclude(
it,
)
}
.then(modifier), .then(modifier),
color = containerColor, color = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -134,7 +126,7 @@ fun Scaffold(
bottomBar = bottomBar, bottomBar = bottomBar,
content = content, content = content,
snackbar = snackbarHost, snackbar = snackbarHost,
contentWindowInsets = remainingWindowInsets, contentWindowInsets = contentWindowInsets,
fab = floatingActionButton, fab = floatingActionButton,
) )
} }
@ -152,7 +144,6 @@ fun Scaffold(
* @param bottomBar the content to place at the bottom of the [Scaffold], on top of the * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
* [content], typically a [NavigationBar]. * [content], typically a [NavigationBar].
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ScaffoldLayout( private fun ScaffoldLayout(
fabPosition: FabPosition, fabPosition: FabPosition,
@ -164,7 +155,47 @@ private fun ScaffoldLayout(
contentWindowInsets: WindowInsets, contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit, 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 layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight val layoutHeight = constraints.maxHeight
@ -175,119 +206,117 @@ private fun ScaffoldLayout(
*/ */
val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity) val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
layout(layoutWidth, layoutHeight) { val topInsetsPlaceables = measurables[0].single()
val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection) .measure(looseConstraints)
val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection) val bottomInsetsPlaceables = measurables[1].single()
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout) .measure(looseConstraints)
val startInsetsPlaceables = measurables[2].single()
.measure(looseConstraints)
val endInsetsPlaceables = measurables[3].single()
.measure(looseConstraints)
// Tachiyomi: Add startBar slot for Navigation Rail val startInsetsWidth = startInsetsPlaceables.width
val startBarPlaceables = subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap { val endInsetsWidth = endInsetsPlaceables.width
it.measure(looseConstraints)
}
val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0
// Tachiyomi: layoutWidth after horizontal insets val topInsetsHeight = topInsetsPlaceables.height
val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth val bottomInsetsHeight = bottomInsetsPlaceables.height
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap { // Tachiyomi: Add startBar slot for Navigation Rail
it.measure(topBarConstraints) 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 { val topBarPlaceables = measurables[5]
it.measure(looseConstraints) .fastMap { it.measure(topBarConstraints) }
}
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0 val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
// Tachiyomi: Calculate insets for snackbar placement offset val bottomPlaceablesConstraints = looseConstraints.offset(
val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) { -startInsetsWidth - endInsetsWidth,
(insetLayoutWidth - snackbarWidth) / 2 + leftInset -bottomInsetsHeight,
} else { )
0
}
val fabPlaceables = val snackbarPlaceables = measurables[6]
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable -> .fastMap { it.measure(bottomPlaceablesConstraints) }
measurable.measure(looseConstraints)
}
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0 val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0 val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { val fabPlaceables = measurables[7]
// FAB distance from the left of the layout, taking into account LTR / RTL .fastMap { it.measure(bottomPlaceablesConstraints) }
// Tachiyomi: Calculate insets for fab placement offset
val fabLeftOffset = if (fabPosition == FabPosition.End) { 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) { if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset FabSpacing.roundToPx()
} else { } else {
FabSpacing.roundToPx() + leftInset layoutWidth - FabSpacing.roundToPx() - fabWidth
} }
} else {
leftInset + ((insetLayoutWidth - fabWidth) / 2)
} }
FabPosition.End, FabPosition.EndOverlay -> {
FabPlacement( if (layoutDirection == LayoutDirection.Ltr) {
left = fabLeftOffset, layoutWidth - FabSpacing.roundToPx() - fabWidth
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()
} else { } else {
topBarHeight.toDp() FabSpacing.roundToPx()
}, }
bottom = // Tachiyomi: Also take account of fab height when providing inner padding }
if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) { else -> (layoutWidth - fabWidth) / 2
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) }
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 // Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.fastForEach { bodyContentPlaceables.fastForEach {
it.place(0, 0) it.place(0, 0)
} }
@ -299,50 +328,27 @@ private fun ScaffoldLayout(
} }
snackbarPlaceables.fastForEach { snackbarPlaceables.fastForEach {
it.place( it.place(
snackbarLeft, (layoutWidth - snackbarWidth) / 2 + when (layoutDirection) {
LayoutDirection.Ltr -> startInsetsWidth
LayoutDirection.Rtl -> endInsetsWidth
},
layoutHeight - snackbarOffsetFromBottom, layoutHeight - snackbarOffsetFromBottom,
) )
} }
// The bottom bar is always at the bottom of the layout // The bottom bar is always at the bottom of the layout
bottomBarPlaceables.fastForEach { bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0)) it.place(0, layoutHeight - bottomBarHeight)
} }
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlaceables.fastForEach { fabPlacement?.let { placement ->
it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0)) 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]. * Placement information for a [FloatingActionButton] inside a [Scaffold].
* *
@ -358,12 +364,5 @@ internal class FabPlacement(
val height: 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 // 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 }