mcpField
E
expo-animation

expo-animation

github.com/expo设计与 UI3.5k16.3k 次安装今天更新
代码仓库

关于 expo-animation

expo-animation 是面向 Expo 中 React Native 动画实现的构建技能。它指导移动开发者判断是否需要动画,并选择保持 UI 运行时响应的实现方式,同时涵盖减少动态效果和真机验证。

主要功能

  1. 01动画必要性与目的判断
  2. 02在 UI 运行时使用 Reanimated worklets
  3. 03适配手势的动画模式
  4. 04时长、弹簧与属性选择指南
  5. 05减少动态效果与设备测试

适用场景

  1. 01实现按压和手势反馈
  2. 02构建弹层、模态框和转场
  3. 03避免动画在较慢设备上卡顿

安装

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

$npx skills add https://github.com/expo/skills --skill expo-animation

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
RECIPES.md
agents/
openai.yaml

详细介绍

在 Expo 中构建动画

此技能由 Emil Kowalski 协作创建,也可在 emilkowalski/skills 仓库中找到,那里还有其他实用的动画技能。

这是一个用于 React Native 的构建技能。它将动画需求转化为一种实现方案,确保在真实设备上经受严格审查——不是在模拟器上,也不是在开发模式下的旗舰手机上。

移动平台会在三个方面改变动画,而本技能中的所有内容都由此而来:

  1. 没有悬停状态。 Web 上通过悬停实现的所有交互提示,都必须通过按压、位置变化或完全不实现来呈现。
  2. 有两个运行时。 Worklets(Reanimated 4)明确体现了这一点:React Native 运行时负责渲染 React 和运行应用逻辑;UI 运行时负责让 worklets 在每一帧运行(此外还可以选择使用 worker 运行时处理后台任务)。动画一旦触及 RN 运行时,应用只要执行其他操作就会卡顿。关键在于让动画始终运行在 UI 运行时。
  3. 用户的手指就在元素上。 手势是主要输入方式,因此可中断性和速度衔接不是锦上添花,而是基本要求。

工作准则

你是一名亲自构建动画的资深移动工程师。做出决定,用一句话说明理由,然后编写代码。绝不要把动画方案摆成菜单供人选择。

有两种失败模式,第一种更糟:

  1. 给不该动的东西添加动画。 下面的判断关卡有时就是为了让代码行数为零。
  2. 在错误的线程上为正确的元素添加动画——例如每帧调用一次 setState、使用 PanResponder 或为 height 添加动画。在你的手机上以开发模式运行时看起来没问题,但在三年前的 Android 设备上会掉到 20fps。

硬性规则

  1. 按顺序执行流程。 第 1 步和第 2 步是所有后续工作的前提。
  2. 使用 Reanimated,而不是核心 Animated。 核心 Animated 无法在不经过 bridge 的情况下由手势驱动,而且 useNativeDriver 只支持 transform 和 opacity。Reanimated worklets 在 UI 线程运行,即使 JS 忙碌也能继续运行。
  3. 不要使用估算值。 曲线和弹簧配置都必须取自下方的表格。
  4. 降低动态效果支持必须与动画一同交付,不能留待后续处理。
  5. 必须在你支持的最慢设备上的 release 构建中评估动画体验。 其他任何情况都不能算作已验证。

构建流程

1. 这个效果应该添加动画吗?
频率决策
每天 100 次以上——切换标签页、打开/关闭键盘、滚动、切换设置不添加动画。 使用平台默认效果或不添加效果。到此为止。
每天几十次——按压反馈、列表导航、行选择仅使用几乎察觉不到的效果:时长低于 150ms,或不添加效果
偶尔——工作表、模态框、toast、引导步骤使用标准动画
罕见/首次发生——成功状态、空状态插图、庆祝效果惊喜效果的预算留给这里

切换标签页时绝不使用滑动效果。 标签页是并列关系,而非层级关系——滑动会暗示并不存在的空间深度,而且用户每次使用应用都要反复承受这种效果。animation: 'none'。

如果需求未通过这一关,请明确指出,不要编写代码。

2. 目的是什么?

继续之前,先用一个词概括:反馈、空间一致性、状态指示、避免突兀变化、说明或惊喜(仅适用于罕见情况)。

说不出来?就不要实现。

3. 选择工具——选能满足需求且最简单的

从上往下查找;找到第一个符合需求的选项就停止。

需求工具
由状态驱动、无需手势的变化——按下、切换、颜色变化、值翻转Reanimated CSS transition(在样式中使用 transitionProperty)
循环、多阶段,或无需状态变化便在挂载时播放Reanimated CSS animation(使用 animationName 关键帧)
元素挂载或卸载,或列表重新排列布局动画(entering / exiting / itemLayoutAnimation)
手指触碰的任何内容,或由滚动衍生的任何内容useSharedValue + Gesture + useAnimatedStyle
屏幕间导航Expo Router 中的原生堆栈选项。 切勿自行实现
自成屏幕的底部面板presentation: 'formSheet'——它是真正的 UISheetPresentationController,免费且正确
标签栏NativeTabs(来自 expo-router/unstable-native-tabs)——平台真正的标签栏,自带行为和过渡效果
上下文菜单、按住时的预览Link.Menu / Link.Preview(Expo Router,仅限 iOS)——使用原生菜单和预览,切勿用 JS 重建
折叠为大标题的标题栏原生堆栈中的 headerLargeTitleEnabled(仅限 iOS;headerLargeTitle 已弃用)——不要使用滚动 worklet
下拉刷新RefreshControl——只有当它是标志性交互时才自行实现(参见阈值方案)
跟随键盘移动的 UIreact-native-keyboard-controller——逐帧获取键盘的实际位置,并在 UI 线程上更新
矢量插画、庆祝动画、空状态Lottie——仅用于插画,绝不用来表示 UI 状态
大型动画场景、自由绘图@shopify/react-native-skia——画布,适用于视图层级本身成为瓶颈的情况

只有当值连续变化或可被中断时,才使用共享值。按压缩放是 CSS transition;拖动才是共享值。为两态切换使用 worklet,就像为了淡入淡出而安装一个动效库一样不合适。

依赖项。 使用 npx expo install <package> 安装——它会解析与项目 SDK 匹配的版本,而普通的 npm install 不会:

需求包
动画react-native-reanimated + react-native-worklets
手势react-native-gesture-handler
导航、面板、原生标签栏、菜单expo-router
触觉反馈expo-haptics
跟随键盘移动的 UIreact-native-keyboard-controller(需要在根节点配置 KeyboardProvider——参见键盘方案)
插画、庆祝动画lottie-react-native
超大型动画场景、自定义绘图@shopify/react-native-skia
4. 选择属性
  • transform 和 opacity 不会触发布局。 其他属性都会触发布局计算。width、height、margin、padding、flex、top、left、gap 会在每一帧为该节点 及其同级节点 重新运行 Yoga。
  • 唯一的例外:没有子元素的绝对定位元素——例如标签栏胶囊、进度条填充。它脱离文档流,因此不会让其他内容重新布局;而且对 width 做动画还能保留圆角,这是 scaleX 会破坏的。
  • 切勿使用 scale(0)。 从 scale(0.9–0.97) + opacity: 0 开始。现实世界中没有任何东西会凭空出现。
  • transform 是一个数组,顺序很重要——[{ translateY }, { scale }] 会先移动再缩放;反过来,平移也会被缩放。除非你希望两者相乘,否则应将平移放在前面。
  • Android 阴影使用 elevation,对 elevation 做动画会在每一帧重新渲染阴影。 改为对预先添加阴影的图层做透明度动画。
  • 切勿对 BlurView 的强度做动画。 在 Android 上,这会让模糊效果每一帧都重新渲染。应改为对静态 BlurView 的透明度做交叉淡化。
  • translate 支持百分比,且百分比相对于元素自身的尺寸——translateY('100%') 会让面板移动相当于自身高度的距离,无论其内容是什么。
5. 定时还是弹簧

如果涉及手指操作,就用弹簧。 弹簧会在中断后保留速度;定时曲线则会重新开始。其他情况都用定时。

Reanimated 的弹簧可直接使用 Apple 提供的两个设计参数——用这种形式,不要用质量/刚度/阻尼:

交互配置
默认稳定,无过冲{ duration: 400, dampingRatio: 1 }
拖动后的重新定位/回弹{ duration: 400, dampingRatio: 0.8, velocity }
面板、抽屉{ duration: 300, dampingRatio: 0.8, velocity }
不得越过硬边界添加 overshootClamping: true

只有手势带有动量时才使用弹跳效果。 渐显菜单出现过冲会让人感觉不对;轻甩卡片后出现过冲则恰到好处。

缓动,适用于所有没有手指参与的情况:

情况缓动
进入或退出ease-out
在屏幕上移动/变形ease-in-out
匀速运动(进度、跑马灯)linear
默认ease-out

UI 上绝不要用 ease-in。 它起步缓慢,会延迟用户正在关注的确切时刻。Reanimated 的内置选项和 CSS 的一样不够理想——请使用以下选项:

import { cubicBezier, Easing } from 'react-native-reanimated';

// transitionTimingFunction / animationTimingFunction
const CSS_EASE_OUT = cubicBezier(0.23, 1, 0.32, 1);

// withTiming / .easing(...)
const EASE_OUT = Easing.bezier(0.23, 1, 0.32, 1);      // strong ease-out for UI
const EASE_IN_OUT = Easing.bezier(0.77, 0, 0.175, 1);  // on-screen movement
const EASE_SHEET = Easing.bezier(0.32, 0.72, 0, 1);    // iOS sheet curve

Reanimated 4.1.1 和 4.5.1 不接受原始的 'cubic-bezier(...)' 字符串。CSS 过渡和动画使用 cubicBezier(...);withTiming 和 .easing(...) 使用 Easing.bezier(...)。

时长:

元素时长
按压反馈100–150ms
切换开关、标签、小型状态变化150–200ms
面板、模态框、抽屉弹簧,感知时长约 300ms
屏幕过渡使用平台默认值——不要覆盖

移动端 UI 动画保持在 300ms 以内,与网页端相同。平台自身的过渡更长(iOS 推入过渡为 350ms);导航时与平台保持一致,其他情况则要更快。

6. 避免占用 JS 线程

这是移动端特有的技巧,也是 React Native 动效最容易出问题的地方。

  • 绝不要在手势或滚动处理程序中调用 setState。 每帧触发一次 React 渲染,是 RN 应用卡顿的最大原因。使用 Shared value → useAnimatedStyle,这样 React 完全不会重新渲染。
  • 绝不要在 onUpdate 或滚动处理程序中调度回 RN 运行时。 react-native-worklets 中的 scheduleOnRN(fn, ...args)——用于替代已弃用的 runOnJS(fn)(...args) 的 Reanimated 4 方案——会将调用排入 RN 运行时队列,而在 onUpdate 中每秒会触发 60–120 次。应在 onEnd 中调用,或放在值跨越阈值时触发的 useAnimatedReaction 中。
  • 绝不要在渲染期间读取 Shared value(在 JSX 中调用 translateY.get())。它只是一个不会更新的快照,会悄无声息地失去同步。渲染期间也绝不要写入 Shared value——这会在协调过程中触发,而且一次并非由你引起的重新渲染会再次执行该写入。只在 worklet、处理程序和副作用中访问 Shared value。
  • 使用 .get() / .set(),不要用 .value。 两者 API 相同,但 React Compiler 无法追踪直接访问 .value 的形式——Reanimated 文档称 get/set 是编译器安全的方式。set 还支持函数式更新:sv.set((v) => v + 1)。
  • 从 worklet 调用的函数,其第一行必须是 'worklet',否则在设备上运行时会抛出异常,但在调试器中却能正常工作。
7. 按压,而非悬停

网页上的所有悬停交互都必须重新设计,而不是直接移植。

  • 按下时反馈,松开时提交。 等到点按完成后才显示任何内容,会让界面显得迟钝——这正是用户实际感受到的延迟。
  • 任何类似按钮的可按控件都应在 100–150ms 内设置 scale: 0.97,使用 Pressable + CSS 过渡。scale 会带动标签和图标一起缩放,这样才会有真实的触感。全宽列表行是例外:只高亮其背景——整行缩放会让人觉得整个屏幕都在挤压。
  • 触控目标至少为 44×44pt(Android 上为 48dp)。如果视觉元素更小,请添加 hitSlop,不要放大视觉元素。
  • 设置 pressRetentionOffset,避免手指偏移几像素就取消用户本想触发的按压。
  • 只有采用 Material 风格的应用才使用 Android 涟漪效果。 对于自定义设计的应用,两端平台使用相同的缩放效果,比只在一个平台上使用涟漪效果更协调。
8. 触觉反馈

移动端拥有网页所没有的感官维度。少量使用能让应用显得更精致;到处使用则会让用户将其关闭。

时机调用
数值经过一个步进——选择器、滑块定位点、分段控件Haptics.selectionAsync()
某物回弹到位、底部面板卡在定位点、拖动操作提交Haptics.impactAsync(ImpactFeedbackStyle.Light)
重物落下、触发破坏性操作Haptics.impactAsync(ImpactFeedbackStyle.Medium)
操作成功或失败Haptics.notificationAsync(NotificationFeedbackType.Success / Error)

以下三条规则绝对不能违反:

  • 与视觉效果在同一帧触发。 触觉反馈落后于动画会显得像故障,而不是反馈。应在因果事件发生时触发——例如定位点卡住的瞬间,而不是动画结束时。
  • 每次用户操作只触发一次。 滚动时绝不触发,每帧绝不触发,用户未触发的入场动画也绝不触发。
  • 绝不能作为唯一反馈。 许多用户在系统范围内关闭了触觉反馈,而且大多数 Android 设备都没有触觉反馈。视觉效果必须独立传达信息。

在 worklet 中,必须将触觉反馈调度回 RN 运行时:scheduleOnRN(Haptics.selectionAsync)。

9. 减少动态效果与无障碍支持
import { useReducedMotion, ReduceMotion, withSpring } from 'react-native-reanimated';

const reduced = useReducedMotion();
const y = useSharedValue(reduced ? 0 : SHEET_HEIGHT);

// or let each animation decide
withSpring(0, { duration: 300, dampingRatio: 0.8, reduceMotion: ReduceMotion.System });

减少动态效果意味着更少、更轻柔,而不是完全没有:保留用于说明状态变化的不透明度和颜色变化,去掉位移、缩放、视差和过冲。屏幕过渡改为 animation: 'fade'。

文字会缩放。 allowFontScaling 默认开启,因此在默认字号下测量的任何高度,在 200% 字号下都会不准确。绝不要动画到硬编码高度——使用 onLayout 测量,或改为动画变换。

会悄悄导致动态效果失效的设置

当“动画就是不运行”时,先检查以下项目:

  • 通过 Expo 安装,以确保版本与 SDK 匹配:npx expo install react-native-reanimated react-native-worklets。在 Expo 项目中,babel-preset-expo 会自动配置 worklets Babel 插件——无需设置 babel.config.js。只有不使用该预设的纯 RN 项目才需要手动添加插件,而且必须将其放在列表末尾。缺少插件或插件位置错误时,不再会静默回退——运行时会抛出 Failed to create a worklet。
  • GestureHandlerRootView 必须包裹整个应用,否则手势不会生效,也不会报错。
  • Reanimated 4 要求使用新架构。
  • Expo Go 不是用于评估性能的环境。 请在发布构建中评估使用体验;开发构建的 JS 线程足够慢,会掩盖你要查找的问题。

120fps

在支持 ProMotion 的 iPhone 上,除非设置了 CADisableMinimumFrameDurationOnPhone,否则第三方动画的帧率会被限制在 60fps。近期的 Expo SDK 默认会设置此项——请确认它已设置;如果没有,请添加:

{ "expo": { "ios": { "infoPlist": { "CADisableMinimumFrameDurationOnPhone": true } } } }

此时每帧预算为 8ms,而不是 16ms。这也正是 UI 线程动画在移动端比在网页端更重要的原因。

示例方案

对于可直接构建的实现——按压反馈、拖动关闭的底部面板、滑动删除、折叠标题栏、列表项入场、与键盘同步的 UI、标签指示器、屏幕转场——请参阅 RECIPES.md。当请求符合其中一项时,就加载它;从示例方案开始,而不是从空白文件开始。

切勿发布

切勿使用应改用
PanRespondergesture-handler 中的 Gesture.Pan()
在手势或滚动处理器中调用 setState共享值 + useAnimatedStyle
runOnJS(在 Reanimated 4 中已弃用)react-native-worklets 中的 scheduleOnRN
每帧调用一次 scheduleOnRNonEnd,或在达到阈值时使用 useAnimatedReaction
渲染期间读取或写入共享值在 worklet、处理器、effect 中使用 .get() / .set()
用核心库 Animated 实现任何手指会触碰的内容Reanimated
动画化 height / width / margin / flex / toptransform + opacity(绝对定位且无子元素的元素除外)
动画化 BlurView 的强度或 Android elevation对静态图层做交叉淡入淡出
对虚拟化列表行使用 entering为容器添加动画,或使用 itemLayoutAnimation
在 JS 中重建屏幕转场原生栈的 animation
在标签页之间滑动animation: 'none'
对 UI 元素使用 Easing.in(...)Easing.bezier(0.23, 1, 0.32, 1)
在 Reanimated CSS 样式中使用 'cubic-bezier(...)'使用 react-native-reanimated 中的 cubicBezier(...)
入场时使用 scale(0)scale(0.95) + opacity: 0
仅基于距离设置关闭阈值速度 或 距离——轻扫一下就足够
到边界时骤停橡皮筋式阻力
每帧触发一次触觉反馈,或将其作为唯一反馈每次提交触发一次,并始终配合视觉反馈
在 Expo Go 或模拟器中判断手感发布构建,在支持设备中最慢的设备上测试

输出

编写代码。然后用不超过几行的文字说明:

  • 评审结果——频率等级和明确用途。说明你拒绝了什么,以及原因。
  • 组成要素——工具、属性、弹簧或曲线 + 时长、线程。
  • 需要在设备上检查的手感——无法仅凭代码判断手势、速度交接和触觉反馈时机。说明要尝试什么:快速轻扫、中途打断动画、反向操作,并在你手头最慢的 Android 设备上运行。

代码才是交付物。不要把它写成冗长报告。

语气

有主见、简洁。如果诚实的答案是“这不该添加动画”,或“这需要在真实设备上测试后才能判断是否合适”,就直说。

提交反馈

如果你发现此技能存在错误、误导性或过时的信息,请报告,以便 Expo 改进:

npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-animation" "<actionable feedback>"

只有在有具体且可执行的反馈时才提交。请尽可能提供相关背景信息。 如果 Agent 反复无法完成 Expo 任务,或用户不得不介入,请使用 --category evals 提交简短报告,说明任务、预期结果和出错之处,并将相关 Expo package 或 command 作为 --subject。

评论

支持 Markdown