Compose Motion 是 athevon/genjutsu 下的一个子技能(Sub-skill),定位为 Jetpack Compose 动画核心。它会在 Android Compose 与 Compose Multiplatform 项目中被加载,正文给出简明规则,深入内容放在 references/ 目录中。其基线为当前稳定版 Jetpack Compose(1.7+),只使用现代稳定 API,明确不使用 swipeable,也不在 AnimatedContent 更合适的地方使用 animateContentSize 之类的变通做法。
一、用途 该技能用于指导在 Jetpack Compose 中实现动画,帮助开发者根据需求选择合适的动画 API,并遵循推荐的弹簧参数、反模式规避与无障碍要求。
二、主要能力
- API 决策树:按需求选择 API。单值随时间变化用 animateFloatAsState、animateDpAsState、animateColorAsState 等;可见性/挂载卸载用 AnimatedVisibility(visible);状态间交叉淡入淡出用 Crossfade(target);多状态协调用 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。
- 弹簧 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)。刚度常量:VeryLow 200、Low 400、MediumLow 700、Medium 1500、High 10000,越高越快稳定。阻尼常量:HighBouncy 0.2、MediumBouncy 0.5、LowBouncy 0.75、NoBouncy 1.0,低于 1.0 会过冲。弹簧忽略 durationMillis,若需要确定时长应改用 tween。
- animate*AsState:给出示例,强调始终设置 label 以便在 Layout Inspector / Animation Preview 中显示。变体覆盖 Dp、Color、Offset、IntOffset、Size、Rect、Float、Int,以及通过 TwoWayConverter 处理自定义类型的通用 animateValueAsState。
- AnimatedVisibility:挂载/卸载动画,可用 + 组合多个进入/退出过渡,退出应比进入更短更简单。内容 composable 仅在可见或动画期间运行,可安全挂载昂贵子项。
- AnimatedContent:状态驱动的布局切换,transitionSpec 中使用 togetherWith 并行运行进入与退出,SizeTransform(clip = false) 控制容器尺寸变化,targetState 身份不变则不触发过渡。
- Crossfade:仅需淡入淡出时使用;更丰富效果(滑动、缩放、布局感知)用 AnimatedContent。Crossfade 不动画尺寸,容器立即采用新内容尺寸。
- updateTransition:多属性协调,多个属性共享同一过渡时间线,同步结束,每个 animate* 调用可接受自己的 transitionSpec。
- Animatable:手动控制,适用于需要中断(stop())、串联(animateTo 完成后返回)、读取实时速度或启动衰减(animateDecay)的场景,是拖拽后甩动、回弹等流程的命令式出口。
- SharedTransitionLayout(Compose 1.7+):Hero 动画,需要向下传递 SharedTransitionScope 与 AnimatedVisibilityScope 两个作用域,rememberSharedContentState(key) 的 key 必须跨屏匹配,否则不触发动画。
- InfiniteTransition:循环动画,RepeatMode.Restart 每周期跳回起点,RepeatMode.Reverse 往返,需为无障碍提供暂停/关闭路径;一次性衰减用 Animatable.animateDecay。
- 手势速查:点击用 Modifier.clickable;点击+长按用 Modifier.combinedClickable;单轴拖拽用 Modifier.draggable(state, orientation);多点触控/自定义用 Modifier.pointerInput { detectDragGestures };滚动偏移读取用 LazyListState 或 ScrollState.value;滑动关闭/吸附点用 Modifier.anchoredDraggable(state)(Compose 1.6+,替代 swipeable);捏合+平移+旋转用 Modifier.transformable(state)。
三、使用方式 该技能以规则与代码示例形式提供,正文给出简明规则,深入内容放在 references/ 中,包括 shared-transitions.md、gestures-compose.md、recomposition-and-anim.md,并关联 compose-graphics、compose-multiplatform、mobile-principles、motion-principles 等技能。
四、反模式
- 动画布局尺寸而非变换:BAD 是对 width 使用 animateDpAsState,每帧强制布局;GOOD 是通过 graphicsLayer 动画 scale(仅合成,无布局)。若布局尺寸必须变化,用 Modifier.animateContentSize() 或 AnimatedContent。
- LaunchedEffect(true)/LaunchedEffect(Unit) 隐藏输入:BAD 是只运行一次但会在调用方替换 composable 实例时意外触发,且 lambda 捕获可能过期;GOOD 是显式 key 绑定触发器。
- LazyColumn 中带 item 动画时忘记 key:BAD 是插入/删除/重排时 item 动画到错误槽位;GOOD 是用稳定 id 作 key,并用 Modifier.animateItem(Compose 1.7+,替代 animateItemPlacement)。
- 在滚动列表项中嵌套 AnimatedContent:BAD 是每个 item 运行自己的过渡图,滚动卡顿;GOOD 是提升状态,只对行上变化的属性做动画。规则是重动画容器(AnimatedContent、SharedTransitionLayout)应放在屏幕作用域,而非每行。
五、减弱动效 提供 rememberReduceMotion() 示例,通过 Settings.Global.ANIMATOR_DURATION_SCALE 读取动画时长缩放,为 0 时视为减弱动效,animationSpec 使用 snap() 而非 spring()。更简洁的调用是 ValueAnimator.areAnimatorsEnabled()(API 26+),当缩放为 0 时返回 false,该缩放由开发者选项“动画关闭”、省电模式和用户可访问性中的“移除动画”开关设置。
六、适用场景与限制 适用于 Android Compose 与 Compose Multiplatform 项目中的动画实现与审查。限制包括:基线为 Jetpack Compose 1.7+,只使用现代稳定 API,不使用 swipeable;部分 API 有版本要求(如 anchoredDraggable 为 Compose 1.6+,SharedTransitionLayout 与 Modifier.animateItem 为 Compose 1.7+);弹簧忽略 durationMillis;Crossfade 不动画尺寸。