Merge pull request 'feat(components): navigation slot, full-bleed content and scrolling actions for the onboarding shell' (#4) from feat/onboarding-shell into main
This commit is contained in:
@@ -33,6 +33,9 @@ import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** Standard touch target, reserved for the shell's navigation slot. */
|
||||
private val NavigationSlotSize = 48.dp
|
||||
|
||||
/** MD3 8dp spacing scale shared by the onboarding screens. */
|
||||
object OnboardingSpace {
|
||||
val xs = 8.dp
|
||||
@@ -50,8 +53,19 @@ object OnboardingSpace {
|
||||
* as one family while keeping their own mark.
|
||||
*
|
||||
* A multi-step flow passes [progress] (see [OnboardingProgress]), which is
|
||||
* pinned above the scrolling content. [topSpacing] gives a step whose hero is a
|
||||
* large picture rather than a mark room to breathe without overflowing.
|
||||
* pinned above the scrolling content, and [navigationIcon] for a step that can
|
||||
* be stepped back from. The navigation slot's width is reserved whether or not
|
||||
* it is filled, and mirrored on the other side, so the progress stays centred
|
||||
* and does not shift as steps come and go.
|
||||
*
|
||||
* [topSpacing] gives a step whose hero is a large picture rather than a mark
|
||||
* room to breathe. [contentPadding] is the scrolling column's own horizontal
|
||||
* inset — a step built from grouped rows passes 0, since those carry their own.
|
||||
*
|
||||
* [scrollingActions] moves the actions off the bottom bar and onto the end of
|
||||
* the scrolling content, for a step whose content is taller than the screen: a
|
||||
* pinned bar there eats the room the content needs and hides the fact that
|
||||
* there is more of it below.
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingScaffold(
|
||||
@@ -59,34 +73,40 @@ fun OnboardingScaffold(
|
||||
actions: @Composable ColumnScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
progress: (@Composable () -> Unit)? = null,
|
||||
navigationIcon: (@Composable () -> Unit)? = null,
|
||||
topSpacing: Dp = OnboardingSpace.xl,
|
||||
contentPadding: Dp = OnboardingSpace.md,
|
||||
scrollingActions: Boolean = false,
|
||||
body: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
if (progress != null) {
|
||||
Box(
|
||||
if (progress != null || navigationIcon != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm),
|
||||
.padding(horizontal = OnboardingSpace.xs, vertical = OnboardingSpace.sm),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
progress()
|
||||
Box(Modifier.size(NavigationSlotSize)) { navigationIcon?.invoke() }
|
||||
Box(Modifier.weight(1f)) { progress?.invoke() }
|
||||
Spacer(Modifier.width(NavigationSlotSize))
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
content = actions,
|
||||
)
|
||||
// Pinned actions carry the navigation-bar inset themselves; scrolling
|
||||
// ones get it from the scaffold's own content padding.
|
||||
if (!scrollingActions) {
|
||||
ActionsColumn(
|
||||
horizontalPadding = OnboardingSpace.md,
|
||||
insetBottom = true,
|
||||
content = actions,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -94,18 +114,45 @@ fun OnboardingScaffold(
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = OnboardingSpace.md),
|
||||
.padding(horizontal = contentPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(Modifier.height(topSpacing))
|
||||
hero()
|
||||
Spacer(Modifier.height(OnboardingSpace.lg))
|
||||
body()
|
||||
// No inset of its own: scrolling actions sit inside the content
|
||||
// column, so they line up with whatever is above them.
|
||||
if (scrollingActions) {
|
||||
ActionsColumn(
|
||||
horizontalPadding = 0.dp,
|
||||
insetBottom = false,
|
||||
content = actions,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(OnboardingSpace.md))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The call-to-action stack, wherever the shell has put it. */
|
||||
@Composable
|
||||
private fun ActionsColumn(
|
||||
horizontalPadding: Dp,
|
||||
insetBottom: Boolean,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (insetBottom) Modifier.navigationBarsPadding() else Modifier)
|
||||
.padding(horizontal = horizontalPadding, vertical = OnboardingSpace.sm),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a multi-step onboarding flow has got to: one capsule per step, filled up
|
||||
* to and including [step] (1-based), with [label] — the caller's own localised
|
||||
|
||||
Reference in New Issue
Block a user