RN 动画的两种运行模式
React Native 动画存在一个根本性的性能挑战:JS 线程和 UI 线程是分离的。当 JS 线程忙于处理业务逻辑、网络请求、React 渲染时,动画帧就会丢失,表现为卡顿和掉帧。
解决方案是将动画逻辑"移到" UI 线程执行,即使 JS 线程完全冻结,动画也能保持流畅。这就是 Reanimated 3 的核心价值。在 RN 0.77.x + 新架构下,Reanimated 与 JSI 的集成更加紧密,worklet 函数的性能进一步提升。
核心名词解释
Animated API
RN 内置动画库。通过 useNativeDriver: true 可以让部分属性动画(transform、opacity)在 UI 线程运行,但不支持 layout 属性(width、height、padding 等)的原生驱动。适合简单动画,复杂场景用 Reanimated。
Reanimated 3
Software Mansion 维护的第三方动画库,所有动画逻辑通过 worklet 在 UI 线程执行,不依赖 JS 线程。支持任意样式属性的动画(包括 width、height 等 Animated API 不支持的属性)。是目前 RN 动画的最佳实践。
worklet
用 'worklet' 指令标记的函数,由 Reanimated Babel 插件在构建时序列化为可在 UI 线程运行的字节码。worklet 内部不能访问 JS 上下文(如 useState、外部模块的引用),只能用 SharedValue 等跨线程原语。
useSharedValue
Reanimated 的跨线程共享值,可同时在 JS 线程和 UI 线程读写。通过 .value 属性访问(.value = x 触发更新)。是 Reanimated 所有动画的基础驱动力——相当于 Animated.Value,但跨线程。
withTiming / withSpring / withDecay
Reanimated 动画函数,在 UI 线程执行。withTiming 是线性/缓动动画(精确控制时长和缓动曲线),withSpring 是弹簧物理动画(自然弹性),withDecay 是惯性滑动(根据初速度逐渐减速)。
useAnimatedStyle
将 SharedValue 绑定到组件样式的 Hook,返回一个动态样式对象。内部函数是 worklet,在 UI 线程执行,当 SharedValue 变化时自动重算样式并更新原生视图,不经过 JS 线程。
Gesture Handler
Software Mansion 维护的手势识别库,在原生层(iOS UIGestureRecognizer / Android GestureDetector)识别手势,避免通过 Bridge 传递事件带来的延迟。支持 Pan、Tap、Pinch、Rotation、Fling 等手势类型,以及手势组合(Simultaneous/Exclusive)。
interpolate
将一个值从输入范围映射到输出范围。例如:将拖拽距离(0-300px)映射为旋转角度(0-360deg)或透明度(1-0)。内置 Extrapolation.CLAMP(夹紧,超出范围时取边界值)避免极端值。
Layout Animation(布局动画)
Reanimated 提供的布局变化动画:元素进入(entering)、退出(exiting)、布局改变(layout)时自动插值动画。无需手动计算,只需给组件声明动画类型即可(如 FadeIn、SlideInRight)。
JS 驱动 vs UI 线程动画:原理对比
JS 驱动动画(旧方式,Animated API 不加 useNativeDriver)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
每帧(16ms):
JS Thread → 计算新动画值 → Bridge(序列化)→ UI Thread → 更新样式
问题:JS Thread 忙碌时(执行业务逻辑、处理网络响应)
这一帧的动画计算被推迟,UI Thread 没有收到新样式 → 掉帧!
──────────────────────────────────────────────────────
UI 线程动画(Reanimated 3 worklet)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JS Thread(可以很忙) UI Thread(独立,始终 60fps)
┌─────────────────┐ ┌────┬────┬────┬────┬────┐
│ React 渲染 │ │ f1 │ f2 │ f3 │ f4 │ f5 │
│ 网络请求处理 │ │动画│动画│动画│动画│动画│
│ 状态更新 │ └────┴────┴────┴────┴────┘
└─────────────────┘ ↑ worklet 在这里执行
SharedValue 跨线程共享
Animated API(内置方案)
Animated API 是 React Native 内置的动画解决方案,无需额外安装。适合简单的渐入渐出、位移动画。关键规则:始终设置 useNativeDriver: true,让动画在 UI 线程运行。
import { Animated, Easing, StyleSheet } from 'react-native';
import { useRef, useEffect } from 'react';
export function FadeInCard({ children }: { children: React.ReactNode }) {
// useRef 保存 Animated.Value,避免每次渲染重建
// 注意:不能用 useState 保存 Animated.Value,因为它本身就是可变的
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(20)).current;
useEffect(() => {
// Animated.parallel:多个动画同时运行
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: 400,
easing: Easing.out(Easing.cubic), // 先快后慢的缓出曲线
useNativeDriver: true, // 必须!让动画跑在 UI 线程
}),
Animated.timing(translateY, {
toValue: 0,
duration: 400,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
]).start(); // .start() 触发动画执行
}, []);
return (
<Animated.View {/* 必须用 Animated.View,普通 View 不支持动画 */}
style={[
styles.card,
{
opacity,
transform: [{ translateY }],
},
]}
>
{children}
</Animated.View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#0d2137',
borderRadius: 12,
padding: 16,
},
});
useNativeDriver 的限制
useNativeDriver: true 只支持 transform(translateX/Y/scale/rotate)和 opacity 的原生驱动动画。不支持 width、height、backgroundColor、padding、margin 等布局属性。如果需要动画这些属性,必须使用 Reanimated 3,或者接受 JS 线程驱动(性能较差)。
Reanimated 3:UI 线程动画全解
import Animated, {
useSharedValue, // 跨线程共享值
useAnimatedStyle, // 将 SharedValue 绑定到样式
withSpring, // 弹簧动画
withTiming, // 线性/缓动动画
withSequence, // 顺序动画
interpolate, // 值映射
Extrapolation,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export function DraggableCard() {
// SharedValue:可在 JS 和 UI 线程同时读写的值
// 通过 .value = x 赋值(不是 useState 的 setter!)
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
// Pan 手势:监听拖拽
// 所有回调函数自动识别为 worklet(在 UI 线程执行)
const gesture = Gesture.Pan()
.onBegin(() => {
// 按下时:弹簧放大(视觉反馈)
scale.value = withSpring(1.05, { damping: 15, stiffness: 300 });
})
.onUpdate((event) => {
// 拖拽中:直接跟随手指位置(无动画,立即响应)
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
// 松手:弹回原位(弹簧动画)
translateX.value = withSpring(0, { damping: 20, stiffness: 200 });
translateY.value = withSpring(0, { damping: 20, stiffness: 200 });
scale.value = withSpring(1);
});
// useAnimatedStyle:将 SharedValue 绑定为组件样式
// 内部函数是 worklet,每次 SharedValue 变化时在 UI 线程重新执行
const animatedStyle = useAnimatedStyle(() => {
// interpolate:将 X 位移(-150 到 150)映射为旋转角度(-15° 到 15°)
// Extrapolation.CLAMP:超出范围时取边界值(不会超过 ±15°)
const rotation = interpolate(
translateX.value,
[-150, 0, 150],
[-15, 0, 15],
Extrapolation.CLAMP
);
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
{ rotate: `${rotation}deg` },
],
};
});
return (
{/* GestureDetector 包裹需要手势的组件 */}
<GestureDetector gesture={gesture}>
{/* Animated.View:Reanimated 的动画容器 */}
<Animated.View style={[styles.card, animatedStyle]}>
<Text style={styles.text}>拖拽我!</Text>
</Animated.View>
</GestureDetector>
);
}
布局动画:进入/退出动画
Reanimated 的 Layout Animation 让元素的出现、消失和布局变化自动有动画,无需手动管理动画值。
import Animated, {
FadeIn, // 淡入进入
FadeOut, // 淡出退出
SlideInRight, // 从右侧滑入
SlideOutLeft, // 向左侧滑出
ZoomIn, // 缩放进入
Layout, // 布局改变动画
} from 'react-native-reanimated';
// 通知列表:条目删除时有滑出动画,其他条目有重排动画
function NotificationList({ items }: { items: Notification[] }) {
return (
<View>
{items.map((item, index) => (
<Animated.View
key={item.id}
entering={SlideInRight.delay(index * 50)} // 延迟进入(错落感)
exiting={SlideOutLeft} // 删除时滑出
layout={Layout.springify()} // 其他元素重排时弹簧动画
style={styles.notificationItem}
>
<Text>{item.message}</Text>
</Animated.View>
))}
</View>
);
}
// 自定义进入动画(弹簧效果)
const MyEntering = ZoomIn
.springify() // 使用弹簧插值
.damping(15) // 阻尼(越小越弹)
.stiffness(200) // 刚度(越大越快)
.delay(100); // 延迟 100ms 后开始
滑动删除(Swipeable)
// 使用 Reanimated + Gesture Handler 实现 iOS 风格的左滑删除
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue, useAnimatedStyle,
withSpring, withTiming, runOnJS,
} from 'react-native-reanimated';
import { Dimensions } from 'react-native';
const SCREEN_WIDTH = Dimensions.get('window').width;
const DELETE_THRESHOLD = -SCREEN_WIDTH * 0.4; // 滑动超过 40% 宽度触发删除
function SwipeableItem({ item, onDelete }: { item: Item; onDelete: () => void }) {
const translateX = useSharedValue(0);
const gesture = Gesture.Pan()
.activeOffsetX([-10, 10]) // 水平偏移超过 10px 才激活手势
.onUpdate((event) => {
// 只允许向左滑(负方向),并限制最大滑动距离
translateX.value = Math.max(event.translationX, -SCREEN_WIDTH * 0.5);
})
.onEnd(() => {
if (translateX.value < DELETE_THRESHOLD) {
// 滑动超过阈值:继续滑出屏幕,然后触发删除回调
// runOnJS:在 worklet 中调用 JS 函数(跨线程)
translateX.value = withTiming(-SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(onDelete)();
});
} else {
// 未达到阈值:弹回原位
translateX.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.item, animatedStyle]}>
<Text>{item.title}</Text>
</Animated.View>
</GestureDetector>
);
}
runOnJS 使用注意
runOnJS 用于在 worklet(UI 线程)中调用普通 JS 函数(如 setState、onDelete 回调)。它的执行是异步的——会在下一帧的 JS 线程执行,不是立即执行。不要在 runOnJS 的函数中执行耗时操作,也不要依赖其返回值。
Reanimated 安装注意
Reanimated 3 需要在
babel.config.js 中添加插件:plugins: ['react-native-reanimated/plugin'],且必须放在所有插件的最后。修改 babel 配置后需要清除缓存重启:npx expo start --clear。忘记清除缓存是 Reanimated 最常见的安装问题。
本章小结
RN 动画的选择策略:① 简单淡入/位移动画 → Animated API + useNativeDriver: true;② 复杂动画(布局属性、物理弹簧、手势联动)→ Reanimated 3;③ 手势识别(拖拽/捏合/旋转)→ react-native-gesture-handler;④ 列表条目进入/退出动画 → Reanimated Layout Animation。核心原则:动画逻辑尽量在 UI 线程执行,不依赖 JS 线程,这样即使 JS 线程繁忙也能保持 60fps。