Skip to main content
ClaudeWave
Skill1.2k estrellas del repoactualizado 3d ago

compose-motion

Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.

Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/Jwuthri/Tracely-ai /tmp/compose-motion && cp -r /tmp/compose-motion/.agents/skills/compose-motion ~/.claude/skills/compose-motion
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# Compose Motion - Sub-skill

> Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects.
> Concise rules here. Deep-dive in `references/`.
> Baseline: Compose 1.7+ (April 2026 stable). Modern stable APIs only - no `swipeable`, no `animateContentSize` hacks where `AnimatedContent` is correct.

---

## API Decision Tree

| Need | API |
|---|---|
| Single value over time | `animateFloatAsState`, `animateDpAsState`, `animateColorAsState`, etc. |
| Visibility / mount-unmount | `AnimatedVisibility(visible) { ... }` |
| Crossfade between states | `Crossfade(target) { state -> ... }` |
| Multi-state coordinated | `updateTransition(target).animateFloat { ... }` |
| Manual control / interruption | `Animatable(initialValue)` + `animateTo(...)` |
| Looping / infinite | `rememberInfiniteTransition().animateFloat(...)` |
| Shared elements | `SharedTransitionLayout` + `Modifier.sharedElement(...)` (Compose 1.7+) |
| Layout swap with anim | `AnimatedContent(target) { ... }` |
| Drag / swipe | `Modifier.draggable` + `Animatable.snapTo`/`animateTo`, or `Modifier.anchoredDraggable` for snap-points |

**Rule:** climb the ladder only when needed. `animate*AsState` covers 70% of cases. Reach for `Animatable` only when you need to interrupt, chain, or read velocity.

---

## Spring API (opinionated defaults)

| Use | Spec |
|---|---|
| UI snap (modal, drawer, tab) | `spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)` |
| Tactile (button press release, toggle) | `spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)` |
| Bouncy reveal (toast, FAB, success) | `spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)` |
| Drag follow (1:1 finger tracking) | `spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)` |

Stiffness constants: `VeryLow` 200, `Low` 400, `MediumLow` 700, `Medium` 1500, `High` 10000. Higher = faster settle. Damping constants: `HighBouncy` 0.2, `MediumBouncy` 0.5, `LowBouncy` 0.75, `NoBouncy` 1.0. Below 1.0 overshoots. Springs ignore `durationMillis`; if you need a deterministic duration, use `tween(...)` instead.

---

## `animate*AsState` - The Bread and Butter

```kotlin
val targetAlpha = if (visible) 1f else 0f
val alpha by animateFloatAsState(
    targetValue = targetAlpha,
    animationSpec = spring(stiffness = Spring.StiffnessMedium),
    label = "alpha",
)
Box(modifier = Modifier.alpha(alpha))
```

The `label` shows up in Layout Inspector / Animation Preview - always set it, future-you will thank present-you. Variants ship for `Dp`, `Color`, `Offset`, `IntOffset`, `Size`, `Rect`, `Float`, `Int`, and a generic `animateValueAsState` for custom types via `TwoWayConverter`.

---

## `AnimatedVisibility` - Mount / Unmount with Anim

```kotlin
AnimatedVisibility(
    visible = expanded,
    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(animationSpec = tween(150)),
) {
    Panel()
}
```

Combine multiple enter/exit transitions with `+`. Respect motion-principles: exit shorter and simpler than enter (here 150ms tween fade vs full slide+fade enter). The content composable only runs while visible OR animating - safe to mount expensive children inside.

---

## `AnimatedContent` - State-Driven Layout Swap

```kotlin
AnimatedContent(
    targetState = currentTab,
    transitionSpec = {
        (slideInHorizontally { it } + fadeIn()) togetherWith
            (slideOutHorizontally { -it } + fadeOut())
    },
    label = "tabs",
) { tab ->
    TabContent(tab)
}
```

`togetherWith` runs enter and exit in parallel; `using SizeTransform(clip = false)` controls how the container resizes between contents. Keys matter: if `targetState` doesn't change identity, no transition fires.

---

## `Crossfade` - Simple Fade Between States

```kotlin
Crossfade(targetState = isLoading, label = "loadState") { loading ->
    if (loading) Spinner() else Content()
}
```

Use when you only need a fade. For anything richer (slide, scale, layout-aware), reach for `AnimatedContent`. `Crossfade` does NOT animate size - the container takes the size of the new content immediately.

---

## `updateTransition` - Multi-Property Coordinated

```kotlin
val transition = updateTransition(targetState = expanded, label = "expand")
val width by transition.animateDp(label = "width") { if (it) 300.dp else 100.dp }
val color by transition.animateColor(label = "color") { if (it) Color.Blue else Color.Gray }
val corner by transition.animateDp(label = "corner") { if (it) 24.dp else 8.dp }

Box(
    Modifier
        .width(width)
        .background(color, RoundedCornerShape(corner)),
)
```

Use when several properties animate together based on the same state. All children share the same transition timeline, so they finish in sync. Each `animate*` call accepts its own `transitionSpec` lambda for per-property tuning.

---

## `Animatable` - Manual Control

```kotlin
val offsetX = remember { Animatable(0f) }
LaunchedEffect(triggerEvent) {
    offsetX.animateTo(100f, spring())
    offsetX.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
}
Box(Modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) })
```

Reach for `Animatable` when you need to interrupt (`stop()`), chain (`animateTo` returns when finished), read live velocity, or kick off decay (`animateDecay`). It is the imperative escape hatch for drag-then-fling, snap-back, and any flow `animate*AsState` cannot express.

---

## `SharedTransitionLayout` (Compose 1.7+) - Hero Animations

```kotlin
SharedTransitionLayout {
    AnimatedContent(targetState = currentScreen, label = "nav") { screen ->
        when (screen) {
            Screen.List -> ListScreen(
                sharedTransitionScope = this@SharedTransitionLayout,
                animatedVisibilityScope = this@AnimatedContent,
            )
            is Screen.Detail -> DetailScreen(