compose-motion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose Motion - Sub-skill

Compose Motion - 子技能

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

Jetpack Compose 动画核心内容。适用于 Android Compose 和 Compose Multiplatform 项目。 此处为简明规则,深入内容请查看
references/
目录。 基准版本:当前稳定版 Jetpack Compose(1.7+)。仅使用现代稳定 API——不使用
swipeable
,在
AnimatedContent
适用场景下不使用
animateContentSize
这类临时方案。

API Decision Tree

API 决策树

NeedAPI
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.

需求API
单个值随时间变化
animateFloatAsState
animateDpAsState
animateColorAsState
可见性 / 挂载-卸载
AnimatedVisibility(visible) { ... }
状态间淡入淡出切换
Crossfade(target) { state -> ... }
多状态协同动画
updateTransition(target).animateFloat { ... }
手动控制 / 中断动画
Animatable(initialValue)
+
animateTo(...)
循环 / 无限动画
rememberInfiniteTransition().animateFloat(...)
共享元素动画
SharedTransitionLayout
+
Modifier.sharedElement(...)
(Compose 1.7+)
带动画的布局切换
AnimatedContent(target) { ... }
拖拽 / 滑动
Modifier.draggable
+
Animatable.snapTo
/
animateTo
,如需吸附点则使用
Modifier.anchoredDraggable
规则: 仅在必要时使用更复杂的API。
animate*AsState
可覆盖70%的场景。仅当需要中断、链式调用或读取速度时,才使用
Animatable

Spring API (opinionated defaults)

Spring API(推荐默认配置)

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:
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.

使用场景参数配置
UI 快速切换(弹窗、抽屉、标签页)
spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)
触觉反馈(按钮按压释放、开关)
spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)
弹性展示(提示框、悬浮按钮、成功提示)
spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
拖拽跟随(1:1手指追踪)
spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)
刚度常量:
VeryLow
200、
Low
400、
MediumLow
700、
Medium
1500、
High
10000。数值越高,动画结束越快。阻尼常量:
HighBouncy
0.2、
MediumBouncy
0.5、
LowBouncy
0.75、
NoBouncy
1.0。数值小于1.0时会出现过冲。Spring 动画忽略
durationMillis
;如果需要确定的时长,请使用
tween(...)

animate*AsState
- The Bread and Butter

animate*AsState
- 核心基础

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
.

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))
label
会显示在布局检查器/动画预览中——务必设置它,未来的你会感谢现在的自己。该API支持的类型包括
Dp
Color
Offset
IntOffset
Size
Rect
Float
Int
,还有通用的
animateValueAsState
,可通过
TwoWayConverter
支持自定义类型。

AnimatedVisibility
- Mount / Unmount with Anim

AnimatedVisibility
- 带动画的挂载/卸载

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.

kotlin
AnimatedVisibility(
    visible = expanded,
    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(animationSpec = tween(150)),
) {
    Panel()
}
可使用
+
组合多个进入/退出过渡动画。遵循动效原则:退出动画比进入动画更短更简洁(此处退出为150ms的淡入淡出,进入为完整的滑动+淡入)。内容组件仅在可见或动画运行时执行——可安全地在内部挂载开销较大的子组件。

AnimatedContent
- State-Driven Layout Swap

AnimatedContent
- 状态驱动的布局切换

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.

kotlin
AnimatedContent(
    targetState = currentTab,
    transitionSpec = {
        (slideInHorizontally { it } + fadeIn()) togetherWith
            (slideOutHorizontally { -it } + fadeOut())
    },
    label = "tabs",
) { tab ->
    TabContent(tab)
}
togetherWith
会并行执行进入和退出动画;
using SizeTransform(clip = false)
用于控制容器在不同内容间的尺寸变化。Key非常重要:如果
targetState
的标识未改变,则不会触发过渡动画。

Crossfade
- Simple Fade Between States

Crossfade
- 状态间的简单淡入淡出

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.

kotlin
Crossfade(targetState = isLoading, label = "loadState") { loading ->
    if (loading) Spinner() else Content()
}
仅当需要淡入淡出效果时使用。如果需要更丰富的动效(滑动、缩放、布局感知),请使用
AnimatedContent
Crossfade
不会动画化尺寸——容器会立即切换为新内容的尺寸。

updateTransition
- Multi-Property Coordinated

updateTransition
- 多属性协同动画

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.

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)),
)
当多个属性基于同一状态进行协同动画时使用。所有子属性共享同一动画时间线,因此会同步完成。每个
animate*
调用可接受独立的
transitionSpec
表达式,用于调整单个属性的动画效果。

Animatable
- Manual Control

Animatable
- 手动控制动画

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.

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) })
当需要中断(
stop()
)、链式调用(
animateTo
完成后返回)、读取实时速度或触发衰减动画(
animateDecay
)时,使用
Animatable
。它是处理拖拽后抛射、回弹等
animate*AsState
无法实现的场景的命令式解决方案。

SharedTransitionLayout
(Compose 1.7+) - Hero Animations

SharedTransitionLayout
(Compose 1.7+)- 英雄动画

kotlin
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
.

kotlin
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,
            )
        }
    }
}

// 在ListScreen的卡片图片中:
with(sharedTransitionScope) {
    Image(
        painter = painter,
        contentDescription = null,
        modifier = Modifier.sharedElement(
            state = rememberSharedContentState(key = "hero-${item.id}"),
            animatedVisibilityScope = animatedVisibilityScope,
        ),
    )
}
需要传递两个作用域:
SharedTransitionScope
Modifier.sharedElement
扩展方法所在的作用域)和
AnimatedVisibilityScope
(驱动过渡动画的可见性上下文)。不同页面间的
rememberSharedContentState(key)
必须匹配,否则不会触发动画。深入内容请查看
references/shared-transitions.md

InfiniteTransition
- Looping

InfiniteTransition
- 循环动画

kotlin
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.

kotlin
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
会在每个循环结束后跳回起始点;
RepeatMode.Reverse
会来回播放。出于无障碍考虑,务必提供暂停/关闭动画的方式(见下方“减少动画”部分)。如需单次衰减动画(抛射),请使用
Animatable.animateDecay

Gestures (quick map)

手势(快速映射)

NeedModifier
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).

需求Modifier
点击
Modifier.clickable(onClick = ...)
点击 + 长按
Modifier.combinedClickable(onClick = ..., onLongClick = ...)
拖拽(单轴)
Modifier.draggable(state, orientation)
多点触控 / 自定义
Modifier.pointerInput { detectDragGestures { ... } }
滚动偏移读取
LazyListState
ScrollState.value
滑动删除 / 吸附点
Modifier.anchoredDraggable(state)
(Compose 1.6+,替代
swipeable
捏合 + 平移 + 旋转
Modifier.transformable(state)
深入内容请查看
references/gestures-compose.md
(NestedScrollConnection、冲突解决、抛射衰减)。

Anti-Patterns (BAD / GOOD)

反模式(错误/正确示例)

1. Animating layout size instead of transform

1. 动画化布局尺寸而非变换属性

kotlin
// 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))
kotlin
// 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
.
kotlin
// 错误 - 对width使用animateDpAsState会导致每一帧都触发布局重排
val w by animateDpAsState(if (expanded) 300.dp else 100.dp)
Box(Modifier.width(w).height(60.dp))
kotlin
// 正确 - 通过graphicsLayer动画化缩放(仅合成层,无布局重排)
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)
        },
)
如果实际布局尺寸必须改变(父组件依赖它),请使用
Modifier.animateContentSize()
让相邻组件也产生动画,或用
AnimatedContent
包裹切换逻辑。

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

2. 使用
LaunchedEffect(true)
/
LaunchedEffect(Unit)
但隐藏输入依赖

kotlin
// 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)
}
kotlin
// 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.
kotlin
// 错误 - 仅运行一次,但当调用者替换组件实例时会意外重新执行。更糟的是:lambda中捕获的任何状态都是过时的。
LaunchedEffect(true) {
    offsetX.animateTo(target)
}
kotlin
// 正确 - 使用与触发条件绑定的显式key
LaunchedEffect(triggerKey) {
    offsetX.animateTo(target)
}
如果你确实需要“仅运行一次”,可刻意使用
LaunchedEffect(Unit)
,但要确保不捕获可变状态——或把捕获的值提升到外部。如有疑问,请使用读取到的值作为key。

3. Forgetting
key
in a
LazyColumn
with item animations

3. 在LazyColumn中使用项动画时忘记设置
key

kotlin
// BAD - on insert/delete/reorder, items animate to wrong slots
LazyColumn {
    items(list) { item ->
        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
    }
}
kotlin
// 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.
kotlin
// 错误 - 插入/删除/重排时,项会动画到错误的位置
LazyColumn {
    items(list) { item ->
        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
    }
}
kotlin
// 正确 - 使用id作为稳定key,加上Modifier.animateItem处理重排动画
LazyColumn {
    items(list, key = { it.id }) { item ->
        Row(modifier = Modifier.animateItem()) {
            AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
        }
    }
}
Modifier.animateItem()
(Compose 1.7+,替代
animateItemPlacement
)会在
key
稳定时自动处理插入/删除/移动动画。

4. Nesting
AnimatedContent
inside scrolling list items

4. 在滚动列表项中嵌套
AnimatedContent

kotlin
// BAD - every item runs its own transition graph; scroll = jank
LazyColumn {
    items(list, key = { it.id }) { item ->
        AnimatedContent(targetState = item.state) { state -> Row(state) }
    }
}
kotlin
// 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.

kotlin
// 错误 - 每个项都运行自己的过渡动画图;滚动时会出现卡顿
LazyColumn {
    items(list, key = { it.id }) { item ->
        AnimatedContent(targetState = item.state) { state -> Row(state) }
    }
}
kotlin
// 正确 - 提取状态,仅对行中变化的属性做动画
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) }
    }
}
规则:重量级动画容器(
AnimatedContent
SharedTransitionLayout
)应放在屏幕级作用域,而非每行中。

Reduced Motion - Respect It

减少动画 - 尊重用户设置

See
../motion-principles/SKILL.md
for the cross-platform doctrine.
kotlin
@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
reads the animator duration scale directly. The cleaner call is
ValueAnimator.areAnimatorsEnabled()
(API 26+), which returns
false
when that scale is 0 - set by the developer-options "Animation off" toggle, Battery Saver, and the user-facing "Remove animations" (Settings -> Accessibility) toggle alike. Deep-dive in
../mobile-principles/references/accessibility-mobile.md
.

跨平台原则请查看
../motion-principles/SKILL.md
kotlin
@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
直接读取动画时长缩放比例。更简洁的调用是
ValueAnimator.areAnimatorsEnabled()
(API 26+),当缩放比例为0时返回
false
——该设置可通过开发者选项“关闭动画”、省电模式或用户可见的“移除动画”(设置 -> 无障碍)开关开启。深入内容请查看
../mobile-principles/references/accessibility-mobile.md

Quick Reference

快速参考

NeedLoad
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

需求查看路径
共享过渡动画深入内容
references/shared-transitions.md
手势 + nestedScroll 模式
references/gestures-compose.md
重组 / 卡顿 / 布局检查器
references/recomposition-and-anim.md
进阶内容(M3 动态效果、AGSL、Canvas)
../compose-graphics/SKILL.md
CMP / KMP 模式
../compose-multiplatform/SKILL.md
移动UX上下文
../mobile-principles/SKILL.md
基础内容(计时、缓动、无障碍)
../motion-principles/SKILL.md

Sources

资料来源