mcpField
C
compose-motion

compose-motion

github.com/athevon设计与 UI698940 次安装3 周前更新
代码仓库

关于 compose-motion

面向 Claude Code、claude.ai 和 Cowork 的 Jetpack Compose 与 Compose Multiplatform 动画技能,将常见界面需求对应到 Compose API,并介绍协调转场、手动控制、手势及减少动态效果。

主要功能

  1. 01Compose 动画 API 选择
  2. 02可见性与内容转场
  3. 03多属性协调动画
  4. 04使用 Animatable 手动控制
  5. 05手势与减少动态效果指导

适用场景

  1. 01为界面元素添加显隐动画
  2. 02协调多个界面属性的变化
  3. 03实现拖动、滑动或吸附行为
  4. 04检查动画的无障碍行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/athevon/genjutsu --skill compose-motion

安装后在 Claude Code 中输入/compose-motion确认已加载。

详细介绍

对版本敏感。 以下所有 API 名称、SDK 要求和浏览器支持声明均已于 2026-09-08 根据一手来源核实。核实依据和时间见 _jutsu/VERSIONS.md。如果该日期已久,请在依据版本号采取行动前重新核实。

Compose Motion - 子技能

Jetpack Compose 动画核心。适用于 Android Compose 和 Compose Multiplatform 项目。 此处列出简明规则。深入内容见 references/。 基准:Jetpack Compose 1.12.0(当前稳定版,2026 年 8 月)——即 Compose BOM 2026.08.x。仅使用现代稳定 API——不使用 swipeable,也不在应使用 AnimatedContent 的场景中采用 animateContentSize 变通方案。标记为“Compose 1.x+”的内容表示引入版本,并非目标版本。


API 决策树

需求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 实现吸附点

规则: 仅在需要时逐级升级。animate*AsState 可满足 70% 的场景。只有需要中断、串联动画或读取速度时,才使用 Animatable。


Spring API(建议默认值)

用途规格
UI 吸附(模态框、抽屉、标签页)spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)
触感反馈(按钮按下后释放、切换开关)spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)
弹性显现(Toast、FAB、成功状态)spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
跟随拖动(与手指 1:1 同步)spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)

刚度常量(androidx.compose.animation.core.Spring):StiffnessVeryLow 为 50f,StiffnessLow 为 200f,StiffnessMediumLow 为 400f,StiffnessMedium 为 1500f(spring() 的默认值),StiffnessHigh 为 10000f。值越高,稳定得越快。阻尼常量:DampingRatioHighBouncy 为 0.2f,DampingRatioMediumBouncy 为 0.5f,DampingRatioLowBouncy 为 0.75f,DampingRatioNoBouncy 为 1.0f(默认值)。低于 1.0 时会产生过冲。弹簧动画会忽略 durationMillis;如果需要确定的时长,请改用 tween(...)。


animate*AsState - 基本功

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 会显示在 Layout Inspector / Animation Preview 中——始终设置它,未来的你会感谢现在的自己。该 API 提供 Dp、Color、Offset、IntOffset、Size、Rect、Float 和 Int 类型的变体;自定义类型则可通过 TwoWayConverter 使用通用的 animateValueAsState。


AnimatedVisibility - 动画显示 / 隐藏

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

使用 + 组合多个进入 / 退出过渡。遵循动效原则:退出动画应比进入动画更短、更简单(此处为 150ms 的淡出 tween,对比完整的滑入加淡入)。内容可组合项只会在可见或动画进行期间运行——因此可以安全地在其中挂载开销较大的子项。


AnimatedContent - 状态驱动的布局切换

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

togetherWith 会并行运行进入和退出动画;using SizeTransform(clip = false) 控制容器在不同内容间切换时如何调整大小。键很重要:如果 targetState 的身份未改变,就不会触发过渡。


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

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

只需要淡入淡出时使用。需要更丰富的效果(滑动、缩放、感知布局的动画)时,请使用 AnimatedContent。Crossfade 不会对尺寸进行动画处理——容器会立即调整为新内容的尺寸。


updateTransition - 多属性协调

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 lambda,以单独调整属性。


Animatable - 手动控制

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 动画

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

这里会向下传递两个作用域:SharedTransitionScope(Modifier.sharedElement 扩展所在的作用域)和 AnimatedVisibilityScope(驱动过渡的可见性上下文)。不同屏幕上的 rememberSharedContentState(key) 键必须一致,否则不会触发动画。详见 references/shared-transitions.md。


InfiniteTransition - 循环动画

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。


手势(速查)

需求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、冲突解决、甩动衰减)。


反模式(不佳 / 良好)

1. 使用变换而非布局尺寸动画
// 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)
        },
)

如果实际布局尺寸必须改变(父级依赖该尺寸),请使用 Modifier.animateContentSize(),让相邻元素也能一起产生动画;或者将内容切换封装在 AnimatedContent 中。

2. 使用带有隐藏输入的 LaunchedEffect(true) / LaunchedEffect(Unit)
// 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)
}

如果确实希望“只运行一次”,请有意使用 LaunchedEffect(Unit),并确保不会捕获变化的状态;或者将捕获的值提升到外部。不确定时,请将读取的值设为键。

3. 在带有条目动画的 LazyColumn 中忘记添加 key
// 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) }
        }
    }
}

只要 key 保持稳定,Modifier.animateItem()(Compose 1.7+,取代 animateItemPlacement)就会自动处理插入、移除和移动。

4. 在可滚动列表项中嵌套 AnimatedContent
// 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) }
    }
}

原则:重量级动画容器(AnimatedContent、SharedTransitionLayout)应放在屏幕级作用域中,而不是每个列表行中。


减少动态效果 - 尊重用户设置

跨平台规范请参阅 ../motion-principles/SKILL.md。

@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。


快速参考

需求加载
深入了解共享转场references/shared-transitions.md
手势与 nestedScroll 模式references/gestures-compose.md
重组/卡顿/Layout Inspectorreferences/recomposition-and-anim.md
高级(M3 Expressive、AGSL、Canvas)../compose-graphics/SKILL.md
CMP/KMP 模式../compose-multiplatform/SKILL.md
移动端 UX 背景../mobile-principles/SKILL.md
基础(时长、缓动、无障碍)../motion-principles/SKILL.md

来源

评论

支持 Markdown