ui: move the action press flourish onto the shape, spin the gear

The press animation turned the glyph; put it on the scalloped cookie
container instead and make it stronger (scale 1→0.82, rotate 0→40°). The
glyph holds upright via a counter-rotation, so the shape spins while a
magnifier or list icon stays readable. New spinIcon opts a glyph into its own
quarter turn — the settings gear uses it, so it reads as a gear cranking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 00:01:07 +02:00
parent 623e533547
commit 245f1db536
2 changed files with 21 additions and 7 deletions

View File

@@ -31,8 +31,10 @@ import androidx.graphics.shapes.RoundedPolygon
* its own [shape] and [containerColor], so a row of them reads as a set of * its own [shape] and [containerColor], so a row of them reads as a set of
* distinct little tokens rather than identical grey glyphs. * distinct little tokens rather than identical grey glyphs.
* *
* Pressing springs the icon down a touch and gives it a small turn — a light * Pressing springs the whole shape down and gives it a turn — the scalloped
* expressive flourish, no library motion APIs needed. * container is what dips and spins, while the glyph inside stays upright (a
* spinning magnifier or list icon would just read as wrong). [spinIcon] opts a
* glyph into turning too, for icons that read well mid-spin like the gear.
*/ */
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -46,15 +48,25 @@ fun ShapedActionButton(
contentColor: Color = MaterialTheme.colorScheme.onTertiaryContainer, contentColor: Color = MaterialTheme.colorScheme.onTertiaryContainer,
size: Dp = 40.dp, size: Dp = 40.dp,
iconSize: Dp = 22.dp, iconSize: Dp = 22.dp,
spinIcon: Boolean = false,
) { ) {
val interaction = remember { MutableInteractionSource() } val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState() val pressed by interaction.collectIsPressedAsState()
val scale by animateFloatAsState(if (pressed) 0.88f else 1f, label = "actionScale") // The shape (the scalloped cookie) turns and dips on press.
val rotation by animateFloatAsState(if (pressed) 24f else 0f, label = "actionRotation") val shapeRotation by animateFloatAsState(if (pressed) 40f else 0f, label = "shapeRotation")
val scale by animateFloatAsState(if (pressed) 0.82f else 1f, label = "shapeScale")
// The glyph's *net* turn: 0 keeps it upright, spinIcon gives it a quarter turn.
val iconRotation by animateFloatAsState(
if (pressed && spinIcon) 90f else 0f,
label = "iconRotation",
)
Surface( Surface(
onClick = onClick, onClick = onClick,
modifier = modifier.size(size), modifier = modifier
.size(size)
.scale(scale)
.rotate(shapeRotation),
shape = shape.toShape(), shape = shape.toShape(),
color = containerColor, color = containerColor,
contentColor = contentColor, contentColor = contentColor,
@@ -64,10 +76,11 @@ fun ShapedActionButton(
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = contentDescription, contentDescription = contentDescription,
// Counter the container's turn so the glyph's net rotation is just
// [iconRotation] — upright by default, a quarter turn for the gear.
modifier = Modifier modifier = Modifier
.size(iconSize) .size(iconSize)
.scale(scale) .rotate(iconRotation - shapeRotation),
.rotate(rotation),
) )
} }
} }

View File

@@ -285,6 +285,7 @@ private fun HomeTopBar(
modifier = Modifier.padding(start = 8.dp), modifier = Modifier.padding(start = 8.dp),
size = 48.dp, size = 48.dp,
iconSize = 26.dp, iconSize = 26.dp,
spinIcon = true,
) )
} }
} }