diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SnackChip.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SnackChip.kt index 76dbe31..0a06c6c 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SnackChip.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SnackChip.kt @@ -5,9 +5,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -15,6 +14,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics @@ -31,17 +32,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -private const val CHIP_ENTER_MILLIS = 160 -private const val CHIP_EXIT_MILLIS = 120 -private const val CHIP_SWAP_MILLIS = 110 - /** The band a snack chip is meant to sit in: the FAB's height, and its margin. */ val SnackChipHeight: Dp = 56.dp val SnackChipMargin: Dp = 16.dp /** * The family's compact confirmation: a rounded pill sized to its content, - * sliding up from the bottom, with an optional trailing action ("Undo"). + * springing open at the bottom, with an optional trailing action ("Undo"). * * Preferred over a full-width snackbar wherever the surface behind it *is* the * confirmation — a task list that just lost a row, a calendar that just moved an @@ -57,6 +54,7 @@ val SnackChipMargin: Dp = 16.dp * The chip carries no timer: how long a confirmation stands, and what expiry * commits, belongs to the caller's own undo window. */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SnackChip( visible: Boolean, @@ -68,14 +66,19 @@ fun SnackChip( color: Color = MaterialTheme.colorScheme.surfaceContainerHighest, contentColor: Color = contentColorFor(color), ) { + // The expressive *fast* springs: a chip is a receipt, not a transition. It + // springs open from its anchored corner rather than sliding in — a slide is + // clipped to the animating bounds, so the pill appears to be painted on from + // one edge, which at this size reads as a glitch rather than as motion. + val spatial = MaterialTheme.motionScheme.fastSpatialSpec() + val effects = MaterialTheme.motionScheme.fastEffectsSpec() + val corner = TransformOrigin(0f, 1f) AnimatedVisibility( visible = visible, - // Deliberately short: a chip is a receipt, not a transition. The default - // visibility spring overshoots and reads as sluggish at this size. - enter = slideInVertically(tween(CHIP_ENTER_MILLIS)) { it } + - fadeIn(tween(CHIP_ENTER_MILLIS)), - exit = slideOutVertically(tween(CHIP_EXIT_MILLIS)) { it } + - fadeOut(tween(CHIP_EXIT_MILLIS)), + enter = scaleIn(spatial, initialScale = 0.8f, transformOrigin = corner) + + fadeIn(effects), + exit = scaleOut(effects, targetScale = 0.9f, transformOrigin = corner) + + fadeOut(effects), modifier = modifier, ) { val action = if (actionLabel != null && onAction != null) actionLabel else null @@ -89,7 +92,7 @@ fun SnackChip( // snapping, so the swap reads as one chip changing its mind. modifier = Modifier .widthIn(max = maxWidth) - .animateContentSize(tween(CHIP_SWAP_MILLIS)), + .animateContentSize(MaterialTheme.motionScheme.fastSpatialSpec()), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -103,10 +106,7 @@ fun SnackChip( ) { AnimatedContent( targetState = message, - transitionSpec = { - fadeIn(tween(CHIP_SWAP_MILLIS)) togetherWith - fadeOut(tween(CHIP_SWAP_MILLIS)) - }, + transitionSpec = { fadeIn(effects) togetherWith fadeOut(effects) }, label = "snack-chip-message", modifier = Modifier.weight(1f, fill = false), ) { text ->