AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Compose Motion

skill-athevon-genjutsu-compose-motion · by AThevon

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

No reviews yet
0 installs
41 views
0.0% view→install

Install

$ agentstack add skill-athevon-genjutsu-compose-motion

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-athevon-genjutsu-compose-motion)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Compose Motion? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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

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

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

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

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

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

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

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

// Inside ListScreen, on the card image:
with(sharedTransitionScope) {
    Image(
        painter = painter,
        contentDescription = null,
        modifier = Modifier.sharedElement(
            state = rememberSharedContentState(key = "hero-${item.id}"),
            animatedVisibilityScope = animatedVisibilityScope,
        ),
    )
}

Two scopes plumbed down: SharedTransitionScope (where Modifier.sharedElement extension lives) and AnimatedVisibilityScope (the visibility context that drives the transition). rememberSharedContentState(key) keys MUST match across screens or no animation fires. Deep-dive in references/shared-transitions.md.


InfiniteTransition - Looping

val infinite = rememberInfiniteTransition(label = "loader")
val rotation by infinite.animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart,
    ),
    label = "rotation",
)
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.rotate(rotation))

RepeatMode.Restart jumps back to start each cycle; RepeatMode.Reverse ping-pongs. Always provide a pause / off path for accessibility (see Reduced Motion section below). For one-shot decay (fling), use Animatable.animateDecay instead.


Gestures (quick map)

| Need | Modifier | |---|---| | Tap | Modifier.clickable(onClick = ...) | | Tap + long-press | Modifier.combinedClickable(onClick = ..., onLongClick = ...) | | Drag (single axis) | Modifier.draggable(state, orientation) | | Multi-touch / custom | Modifier.pointerInput { detectDragGestures { ... } } | | Scroll offset reading | LazyListState or ScrollState.value | | Swipe-to-dismiss / snap points | Modifier.anchoredDraggable(state) (Compose 1.6+, replaces swipeable) | | Pinch + pan + rotate | Modifier.transformable(state) |

Deep-dive in references/gestures-compose.md (NestedScrollConnection, conflict resolution, fling decay).


Anti-Patterns (BAD / GOOD)

1. Animating layout size instead of transform

// BAD - animateDpAsState on width forces a layout pass every frame
val w by animateDpAsState(if (expanded) 300.dp else 100.dp)
Box(Modifier.width(w).height(60.dp))
// GOOD - animate scale via graphicsLayer (composite-only, no layout)
val scale by animateFloatAsState(if (expanded) 3f else 1f, label = "scale")
Box(
    Modifier
        .width(100.dp)
        .height(60.dp)
        .graphicsLayer {
            scaleX = scale
            transformOrigin = TransformOrigin(0f, 0.5f)
        },
)

If the actual layout size MUST change (parent depends on it), use Modifier.animateContentSize() so neighbors animate too, or wrap the swap in AnimatedContent.

2. LaunchedEffect(true) / LaunchedEffect(Unit) with hidden inputs

// BAD - re-runs only once, BUT also fires on every recomposition surprise
//        when callers swap the composable instance. Worse: anything captured
//        in the lambda is stale.
LaunchedEffect(true) {
    offsetX.animateTo(target)
}
// GOOD - explicit key tied to the trigger
LaunchedEffect(triggerKey) {
    offsetX.animateTo(target)
}

If you genuinely want "run once", use LaunchedEffect(Unit) on purpose AND ensure you don't capture varying state - or hoist the captured values out. When in doubt, key on the values you read.

3. Forgetting key in a LazyColumn with item animations

// BAD - on insert/delete/reorder, items animate to wrong slots
LazyColumn {
    items(list) { item ->
        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
    }
}
// GOOD - stable key by id, plus Modifier.animateItem for reorder anim
LazyColumn {
    items(list, key = { it.id }) { item ->
        Row(modifier = Modifier.animateItem()) {
            AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
        }
    }
}

Modifier.animateItem() (Compose 1.7+, replaces animateItemPlacement) handles insert/remove/move automatically when key is stable.

4. Nesting AnimatedContent inside scrolling list items

// BAD - every item runs its own transition graph; scroll = jank
LazyColumn {
    items(list, key = { it.id }) { item ->
        AnimatedContent(targetState = item.state) { state -> Row(state) }
    }
}
// GOOD - lift state, animate only the changing prop on the row
LazyColumn {
    items(list, key = { it.id }) { item ->
        val color by animateColorAsState(if (item.selected) selBg else bg, label = "rowBg")
        Row(Modifier.background(color)) { Content(item) }
    }
}

Rule: heavy animation containers (AnimatedContent, SharedTransitionLayout) belong at screen scope, not per row.


Reduced Motion - Respect It

See ../motion-principles/SKILL.md for the cross-platform doctrine.

@Composable
fun rememberReduceMotion(): Boolean {
    val context = LocalContext.current
    return remember {
        Settings.Global.getFloat(
            context.contentResolver,
            Settings.Global.ANIMATOR_DURATION_SCALE,
            1f,
        ) == 0f
    }
}

val reduce = rememberReduceMotion()
val alpha by animateFloatAsState(
    targetValue = if (visible) 1f else 0f,
    animationSpec = if (reduce) snap() else spring(),
    label = "alpha",
)

ANIMATOR_DURATION_SCALE covers the developer-options "Animations off" toggle. For the user-facing accessibility setting on Android 14+ ("Remove animations" in Settings -> Accessibility), use AccessibilityManager.areTransitionsEnabled() (API 34+). Deep-dive in ../mobile-principles/references/accessibility-mobile.md.


Quick Reference

| Need | Load | |---|---| | Shared transitions deep-dive | references/shared-transitions.md | | Gestures + nestedScroll patterns | references/gestures-compose.md | | Recomposition / jank / Layout Inspector | references/recomposition-and-anim.md | | Advanced (M3 Expressive, AGSL, Canvas) | ../compose-graphics/SKILL.md | | CMP / KMP patterns | ../compose-multiplatform/SKILL.md | | Mobile UX context | ../mobile-principles/SKILL.md | | Foundation (timing, easing, a11y) | ../motion-principles/SKILL.md |


Sources

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.