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.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 ->
|
||||||
|
|||||||
@@ -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) },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user