feat(components): spring the snack chip open
A slide-in is clipped to the animating bounds, so at pill size the chip looked painted on from one edge. It now springs from its anchored corner on the expressive fast specs, which are also quicker than the default visibility spring.
This commit is contained in:
@@ -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<Float>()
|
||||
val effects = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
|
||||
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 ->
|
||||
|
||||
Reference in New Issue
Block a user