components: largeTopBar, gapBelow, enabled

CollapsingScaffold gains largeTopBar (false = pinned single-line bar, the
picker variant); GroupedSurface/GroupedRow gain gapBelow to suppress the 2dp
inter-row gap for a caller that owns spacing; InlineTextField gains enabled
(disabled dims to a locked value). All additive, defaults preserve behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 21:11:25 +02:00
parent bd874c2586
commit 53217f139e
3 changed files with 65 additions and 29 deletions

View File

@@ -19,6 +19,7 @@ import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -30,10 +31,16 @@ import de.jeanlucmakiola.floret.identity.predictiveBack
/** /**
* The family's collapsing scaffold for settings hubs, sub-screens and the * The family's collapsing scaffold for settings hubs, sub-screens and the
* full-screen pickers: a [LargeTopAppBar] that collapses on scroll, a back * full-screen pickers. By default a [LargeTopAppBar] whose title shrinks into
* button, and a vertically scrolling content column. [content] lays out the * the bar as the content scrolls — for settings and sub-screens, where the large
* connected grouped rows. The back content description uses the kit's `back` * header sets the page — over a vertically scrolling content column. [content]
* string, which an app can override with its own (localized) `back`. * lays out the connected grouped rows. The back content description uses the
* kit's `back` string, which an app can override with its own (localized) `back`.
*
* Set [largeTopBar] to false for a pinned, single-line [TopAppBar] instead: the
* title sits in the bar from the start with no expanded header to scroll past.
* Preferred for selection pickers, where the tall header is only empty space
* above a short list.
* *
* Optional extras, all defaulting to the bare scaffold so existing callers are * Optional extras, all defaulting to the bare scaffold so existing callers are
* unchanged: [actions] adds trailing app-bar items; [snackbarHost] hosts * unchanged: [actions] adds trailing app-bar items; [snackbarHost] hosts
@@ -46,13 +53,17 @@ fun CollapsingScaffold(
title: String, title: String,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
largeTopBar: Boolean = true,
predictiveBack: Boolean = false, predictiveBack: Boolean = false,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
val scrollBehavior = val scrollBehavior = if (largeTopBar) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
} else {
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
}
Scaffold( Scaffold(
modifier = modifier modifier = modifier
.then(if (predictiveBack) Modifier.predictiveBack(onBack = onBack) else Modifier) .then(if (predictiveBack) Modifier.predictiveBack(onBack = onBack) else Modifier)
@@ -60,22 +71,35 @@ fun CollapsingScaffold(
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
LargeTopAppBar( val navigationIcon = @Composable {
title = { Text(title) }, IconButton(onClick = onBack) {
navigationIcon = { Icon(
IconButton(onClick = onBack) { Icons.AutoMirrored.Rounded.ArrowBack,
Icon( contentDescription = stringResource(R.string.back),
Icons.AutoMirrored.Rounded.ArrowBack, )
contentDescription = stringResource(R.string.back), }
) }
} if (largeTopBar) {
}, LargeTopAppBar(
actions = actions, title = { Text(title) },
scrollBehavior = scrollBehavior, navigationIcon = navigationIcon,
colors = TopAppBarDefaults.largeTopAppBarColors( actions = actions,
scrolledContainerColor = MaterialTheme.colorScheme.surface, scrollBehavior = scrollBehavior,
), colors = TopAppBarDefaults.largeTopAppBarColors(
) scrolledContainerColor = MaterialTheme.colorScheme.surface,
),
)
} else {
TopAppBar(
title = { Text(title) },
navigationIcon = navigationIcon,
actions = actions,
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.surface,
),
)
}
}, },
snackbarHost = snackbarHost, snackbarHost = snackbarHost,
) { innerPadding -> ) { innerPadding ->

View File

@@ -51,6 +51,10 @@ fun groupedShape(position: Position, full: Dp, small: Dp): Shape = when (positio
* all but the last. Corners morph rounder on press — the expressive shape play. * all but the last. Corners morph rounder on press — the expressive shape play.
* The caller supplies [content] and any horizontal inset via [modifier], so the * The caller supplies [content] and any horizontal inset via [modifier], so the
* same primitive serves both the 16dp-inset lists and full-width detail cards. * same primitive serves both the 16dp-inset lists and full-width detail cards.
*
* Set [gapBelow] to false to suppress that 2dp separation — for a caller that
* owns uniform spacing itself, e.g. [ReorderableColumn], where every slot must
* share the same pitch for the drag maths to line up.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -59,6 +63,7 @@ fun GroupedSurface(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
color: Color = MaterialTheme.colorScheme.surfaceContainerHigh, color: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
gapBelow: Boolean = true,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val interaction = remember { MutableInteractionSource() } val interaction = remember { MutableInteractionSource() }
@@ -66,9 +71,10 @@ fun GroupedSurface(
val full by animateDpAsState(if (pressed) 36.dp else 22.dp, label = "fullCorner") val full by animateDpAsState(if (pressed) 36.dp else 22.dp, label = "fullCorner")
val small by animateDpAsState(if (pressed) 36.dp else 6.dp, label = "smallCorner") val small by animateDpAsState(if (pressed) 36.dp else 6.dp, label = "smallCorner")
val shape = groupedShape(position, full, small) val shape = groupedShape(position, full, small)
val gap = when (position) { val gap = when {
Position.Top, Position.Middle -> Modifier.padding(bottom = 2.dp) !gapBelow -> Modifier
Position.Bottom, Position.Alone -> Modifier position == Position.Top || position == Position.Middle -> Modifier.padding(bottom = 2.dp)
else -> Modifier
} }
val base = modifier.fillMaxWidth().then(gap) val base = modifier.fillMaxWidth().then(gap)
if (onClick != null) { if (onClick != null) {
@@ -87,7 +93,8 @@ fun GroupedSurface(
* headline and summary to the M3 disabled emphasis while leaving the [trailing] * headline and summary to the M3 disabled emphasis while leaving the [trailing]
* control at full opacity — for rows that are present but switched off. * control at full opacity — for rows that are present but switched off.
* [container] overrides the row's tonal colour (e.g. a category tint); ignored * [container] overrides the row's tonal colour (e.g. a category tint); ignored
* when [selected]. * when [selected]. [gapBelow] (see [GroupedSurface]) suppresses the inter-row
* gap for a caller that owns its own uniform spacing, e.g. [ReorderableColumn].
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -100,6 +107,7 @@ fun GroupedRow(
dimmed: Boolean = false, dimmed: Boolean = false,
container: Color? = null, container: Color? = null,
minHeight: Dp = 72.dp, minHeight: Dp = 72.dp,
gapBelow: Boolean = true,
leading: @Composable (() -> Unit)? = null, leading: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null, trailing: @Composable (() -> Unit)? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
@@ -135,6 +143,7 @@ fun GroupedRow(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier.padding(horizontal = 16.dp),
onClick = onClick, onClick = onClick,
color = containerColor, color = containerColor,
gapBelow = gapBelow,
) { ) {
ListItem( ListItem(
headlineContent = { Text(title) }, headlineContent = { Text(title) },

View File

@@ -36,6 +36,7 @@ fun InlineTextField(
textStyle: TextStyle = MaterialTheme.typography.titleMedium, textStyle: TextStyle = MaterialTheme.typography.titleMedium,
singleLine: Boolean = true, singleLine: Boolean = true,
minLines: Int = 1, minLines: Int = 1,
enabled: Boolean = true,
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
capitalization: KeyboardCapitalization = KeyboardCapitalization.Sentences, capitalization: KeyboardCapitalization = KeyboardCapitalization.Sentences,
imeAction: ImeAction = ImeAction.Default, imeAction: ImeAction = ImeAction.Default,
@@ -43,15 +44,17 @@ fun InlineTextField(
onImeAction: (() -> Unit)? = null, onImeAction: (() -> Unit)? = null,
) { ) {
val resolvedStyle = textStyle.copy( val resolvedStyle = textStyle.copy(
color = if (textStyle.color.isSpecified) { color = when {
textStyle.color // A disabled field reads as dimmed, like a locked value.
} else { !enabled -> MaterialTheme.colorScheme.onSurfaceVariant
MaterialTheme.colorScheme.onSurface textStyle.color.isSpecified -> textStyle.color
else -> MaterialTheme.colorScheme.onSurface
}, },
) )
BasicTextField( BasicTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
enabled = enabled,
textStyle = resolvedStyle, textStyle = resolvedStyle,
singleLine = singleLine, singleLine = singleLine,
minLines = minLines, minLines = minLines,