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:
@@ -19,6 +19,7 @@ import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
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
|
||||
* full-screen pickers: a [LargeTopAppBar] that collapses on scroll, a back
|
||||
* button, and a vertically scrolling content column. [content] 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`.
|
||||
* full-screen pickers. By default a [LargeTopAppBar] whose title shrinks into
|
||||
* the bar as the content scrolls — for settings and sub-screens, where the large
|
||||
* header sets the page — over a vertically scrolling content column. [content]
|
||||
* 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
|
||||
* unchanged: [actions] adds trailing app-bar items; [snackbarHost] hosts
|
||||
@@ -46,13 +53,17 @@ fun CollapsingScaffold(
|
||||
title: String,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
largeTopBar: Boolean = true,
|
||||
predictiveBack: Boolean = false,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val scrollBehavior =
|
||||
val scrollBehavior = if (largeTopBar) {
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
} else {
|
||||
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier
|
||||
.then(if (predictiveBack) Modifier.predictiveBack(onBack = onBack) else Modifier)
|
||||
@@ -60,22 +71,35 @@ fun CollapsingScaffold(
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
val navigationIcon = @Composable {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (largeTopBar) {
|
||||
LargeTopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
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,
|
||||
) { innerPadding ->
|
||||
|
||||
@@ -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.
|
||||
* 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.
|
||||
*
|
||||
* 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)
|
||||
@Composable
|
||||
@@ -59,6 +63,7 @@ fun GroupedSurface(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
color: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
gapBelow: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
@@ -66,9 +71,10 @@ fun GroupedSurface(
|
||||
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 shape = groupedShape(position, full, small)
|
||||
val gap = when (position) {
|
||||
Position.Top, Position.Middle -> Modifier.padding(bottom = 2.dp)
|
||||
Position.Bottom, Position.Alone -> Modifier
|
||||
val gap = when {
|
||||
!gapBelow -> Modifier
|
||||
position == Position.Top || position == Position.Middle -> Modifier.padding(bottom = 2.dp)
|
||||
else -> Modifier
|
||||
}
|
||||
val base = modifier.fillMaxWidth().then(gap)
|
||||
if (onClick != null) {
|
||||
@@ -87,7 +93,8 @@ fun GroupedSurface(
|
||||
* headline and summary to the M3 disabled emphasis while leaving the [trailing]
|
||||
* 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
|
||||
* 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)
|
||||
@Composable
|
||||
@@ -100,6 +107,7 @@ fun GroupedRow(
|
||||
dimmed: Boolean = false,
|
||||
container: Color? = null,
|
||||
minHeight: Dp = 72.dp,
|
||||
gapBelow: Boolean = true,
|
||||
leading: @Composable (() -> Unit)? = null,
|
||||
trailing: @Composable (() -> Unit)? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
@@ -135,6 +143,7 @@ fun GroupedRow(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
onClick = onClick,
|
||||
color = containerColor,
|
||||
gapBelow = gapBelow,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
|
||||
@@ -36,6 +36,7 @@ fun InlineTextField(
|
||||
textStyle: TextStyle = MaterialTheme.typography.titleMedium,
|
||||
singleLine: Boolean = true,
|
||||
minLines: Int = 1,
|
||||
enabled: Boolean = true,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
capitalization: KeyboardCapitalization = KeyboardCapitalization.Sentences,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
@@ -43,15 +44,17 @@ fun InlineTextField(
|
||||
onImeAction: (() -> Unit)? = null,
|
||||
) {
|
||||
val resolvedStyle = textStyle.copy(
|
||||
color = if (textStyle.color.isSpecified) {
|
||||
textStyle.color
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
color = when {
|
||||
// A disabled field reads as dimmed, like a locked value.
|
||||
!enabled -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
textStyle.color.isSpecified -> textStyle.color
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
)
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
textStyle = resolvedStyle,
|
||||
singleLine = singleLine,
|
||||
minLines = minLines,
|
||||
|
||||
Reference in New Issue
Block a user