RN动画实现方式深度解析
对比 Animated API、LayoutAnimation 和手势动画三种方案,拆解 `useNativeDriver` 的底层原理与 react-native-reanimated 的未来方向。
一句话概括
RN 有三种动画方案——Animated API(声明式驱动,灵活但需要 JS 参与)、LayoutAnimation(布局变化自动过渡,零 JS 开销但灵活性差)、手势动画(交互式,配合 reanimated 可达 60fps)——选对的方案、开对 useNativeDriver,是 RN 动画不掉帧的唯二法门。
核心知识点
1. Animated API:三种动画类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ① timing:时间驱动的渐变
Animated.timing(value, {
toValue: 1,
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // 自定义贝塞尔曲线
useNativeDriver: true, // 💡 关键!
}).start(({ finished }) => {
if (finished) console.log('完成')
})
// ② spring:物理弹簧模型
Animated.spring(value, {
toValue: 200,
friction: 7, // 摩擦力越大弹跳越少(默认 7)
tension: 40, // 张力越大速度越快(默认 40)
useNativeDriver: true,
}).start()
// ③ decay:惯性减速(配合手势)
Animated.decay(value, {
velocity: 1.5, // 初始速度(手势松手时的速度)
deceleration: 0.997, // 减速率(默认 0.997)
useNativeDriver: true,
}).start()
// 组合:串行 / 并行 / 交错
Animated.sequence([anim1, anim2, anim3]) // 一个接一个
Animated.parallel([anim1, anim2, anim3]) // 同时跑
Animated.stagger(100, [anim1, anim2, anim3]) // 间隔 100ms 依次启动
2. useNativeDriver 的底层原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开启 useNativeDriver: true 后:
JS 线程(仅启动瞬间参与) UI 线程(接管后续所有帧)
│ │
│ .start() │
│ → 通过 Bridge/JSI 发送动画配置 │
│ {toValue, duration, easing} │
│ │ → 创建原生驱动器
│ │ → CADisplayLink(iOS)/
│ │ Choreographer(Android)
│ │ 每帧回调:
│ │ - 计算当前 progress
│ │ - easing 插值
│ │ - 直接更新 View 属性
│ │ - 零 JS 参与!
│ JS 之后可以去做别的事 │
限制:useNativeDriver 只能驱动 transform(translateX/Y、scale、rotate)和 opacity。不能驱动 width、height、left、top、backgroundColor 等布局属性——因为这些需要触发原生侧的 requestLayout(),原生驱动没有这个能力。
变通方案:用 translateY 替代 top,用 scaleX/scaleY 模拟宽高动画,用 opacity + 两个叠放的 View 模拟颜色渐变。
3. LayoutAnimation:零代码的布局过渡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 安卓需要手动开启
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
// 一行代码触发,所有布局变化自动动画化
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setExpanded(!expanded)
// 之后 expanded 控制的 View 出现/消失/移动 全自动过渡
// 精细控制:create(出现) / update(移动) / delete(消失) 分别配置
LayoutAnimation.configureNext({
duration: 400,
create: { type: 'easeInEaseOut', property: 'opacity' },
update: { type: 'spring', springDamping: 0.7 },
delete: { type: 'easeInEaseOut', property: 'scaleXY' },
})
LayoutAnimation 比 Animated + useNativeDriver 更彻底——JS 只调一次 configureNext,后续完全原生自驱动,连动画配置的传输都免了。代价是灵活性极差:只能对 setState 触发的布局变化做过渡,无法做持续变化的交互动画。
4. 手势动画:从 PanResponder 到 reanimated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ❌ PanResponder:事件从原生 → JS 线程处理 → 更新 Animated.Value
// JS 线程一忙就掉帧,"拖拽不跟手"的罪魁祸首
const panResponder = PanResponder.create({
onPanResponderMove: (_, { dx, dy }) => {
pan.setValue({ x: dx, y: dy }) // 在 JS 线程!
},
})
// ✅ react-native-reanimated:手势+动画全在 UI 线程
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'
function DraggableBox() {
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
const gesture = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX // UI 线程!
translateY.value = e.translationY
})
.onEnd(() => {
translateX.value = withSpring(0) // 弹回动画也在 UI 线程
translateY.value = withSpring(0)
})
const style = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
}))
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[{ width: 100, height: 100 }, style]} />
</GestureDetector>
)
}
手势动画选型:PanResponder 够用于低频交互(如按钮拖拽),高频交互(如地图拖拽、图片缩放)必须上 react-native-gesture-handler + react-native-reanimated,全程 UI 线程,JS 线程卡死也不掉帧。
5. 三种方案的选型决策
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 按钮按下反馈 | Pressable style + pressed | 最简单,原生驱动 |
| 页面切换过渡 | Animated + useNativeDriver | 精细控制时序 |
| 列表展开/折叠 | LayoutAnimation | 零代码,自动过渡 |
| 启动引导页序列动画 | Animated.sequence | 步步控制 |
| 拖拽/缩放/旋转 | reanimated + gesture-handler | 全 UI 线程 |
| 骨架屏 shimmer | Animated.loop + native driver | 循环动画 |
其实你每天都在用
Pressable的pressed状态变化就是动画 —style={({ pressed }) => [...]}中的状态切换自带原生驱动的过渡react-navigation的页面切换动画底层就是 Animated API — stack navigator 的 slide、fade 效果都是 Animated.timing- 键盘弹起时
KeyboardAvoidingView的位移 → 就是 LayoutAnimation 的一次实际应用 — 键盘弹起导致布局变化,自动过渡 RefreshControl的下拉刷新动画 → Android 原生驱动、iOS 部分走 Animated — 两端实现不同,但表现一致- Lottie 动画文件(JSON)比代码动画更适合复杂动效 — 设计师用 AE 导出 Lottie JSON,
lottie-react-native直接在原生层播放
常见误解(FAQ)
❌ 误区:「开了 useNativeDriver 动画就一定 60fps」 useNativeDriver 只保证动画计算不在 JS 线程——但如果 Props 更新触发 React 重渲染(比如动画驱动的 transform 变化导致父组件 setState),JS 线程依然可能成为瓶颈。reanimated 的 useAnimatedStyle 完全绕过了 React 的 render 周期,才是真正的”全原生驱动”。
❌ 误区:「width/height 动画不能用原生驱动,所以一定卡」 用 scaleX/scaleY + transformOrigin 可以模拟宽高动画,而且能用 useNativeDriver。视觉效果一致,性能天差地别。面试官问”width 从 100 变 200 怎么优化”,答案就是”改成 scaleX 从 1 到 2”。
❌ 误区:「LayoutAnimation 是旧的 API,应该用 Animated 替代」 两者不是替代关系,是互补。LayoutAnimation 适用于”我不知道哪些 View 会变,但我知道它们会变”的场景——比如一个动态表单增删字段,你没法为每个字段都创建 Animated.Value。LayoutAnimation 一句搞定。
❌ 误区:「动画做完了就完事了,不需要关心 cleanup」 Animated.timing().start() 返回的对象不持有 → 组件卸载后动画继续跑 → 尝试更新已卸载组件的 state → 内存泄漏 + warning。正确的做法是用 useRef 持有动画引用,useEffect return 里 .stop()。
一句话总结
RN 动画从”能用”到”丝滑”的分水岭就两个:useNativeDriver: true 把动画计算送离 JS 线程,react-native-reanimated 把整个动画管线搬到 UI 线程——前者是及格线,后者是优秀线。