athevon/genjutsu4 files

Compose Motion

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

Specification
Skill ID
athevon/genjutsu/compose-motion
Publisher
athevon
Repository
genjutsu
Installs
0
Files
4
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

athevon/genjutsu/compose-motionInstalls these files
  • SKILL.md
  • references/gestures-compose.md
  • references/recomposition-and-anim.md
  • references/shared-transitions.md

What this skill tells the agent

Version-sensitive. Every API name, SDK gate and browser-support claim below was verified on 2026-09-08 against primary sources. What against, and when, is in _jutsu/VERSIONS.md. If that date is old, re-verify before acting on a version number.

Compose Motion - Sub-skill

Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects. Concise rules here. Deep-dive in references/. Baseline: Jetpack Compose 1.12.0 (current stable, Aug 2026) - i.e. Compose BOM 2026.08.x. Modern stable APIs only - no swipeable, no animateContentSize hacks where AnimatedContent is correct. Anything below flagged 'Compose 1.x+' is a since marker, not the target.

API Decision Tree

NeedAPI
Single value over timeanimateFloatAsState, animateDpAsState, animateColorAsState, etc.
Visibility / mount-unmountAnimatedVisibility(visible) { ... }
Crossfade between statesCrossfade(target) { state -> ... }
Multi-state coordinatedupdateTransition(target).animateFloat { ... }
Manual control / interruptionAnimatable(initialValue) + animateTo(...)
Looping / infiniterememberInfiniteTransition().animateFloat(...)
Shared elementsSharedTransitionLayout + Modifier.sharedElement(...) (Compose 1.7+)
Layout swap with animAnimatedContent(target) { ... }
Drag / swipeModifier.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)

UseSpec
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 (androidx.compose.animation.core.Spring): StiffnessVeryLow 50f, StiffnessLow 200f, StiffnessMediumLow 400f, StiffnessMedium 1500f (the spring() default), StiffnessHigh 10000f. Higher = faster settle. Damping constants: DampingRatioHighBouncy 0.2f, DampingRatioMediumBouncy 0.5f, DampingRatioLowBouncy 0.75f, DampingRatioNoBouncy 1.0f (the default). 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(
            sharedContentState = 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))

Use this skill in a project

Open any RiverX project, search the Skills panel for athevon/genjutsu/compose-motion, and install it in one click.

Use this skill