RN 动画的两种运行模式
React Native 动画存在一个根本性的性能挑战:JS 线程和 UI 线程是分离的。当 JS 线程忙于处理业务逻辑、网络请求、React 渲染时,动画帧就会丢失,表现为卡顿和掉帧。
解决方案是将动画逻辑"移到" UI 线程执行,即使 JS 线程完全冻结,动画也能保持流畅。这就是 Reanimated 3 的核心价值。
核心名词解释
Animated API
RN 内置动画库。通过 useNativeDriver: true 可以让部分属性动画(transform、opacity)在 UI 线程运行,但不支持 layout 属性(width、height、padding 等)的原生驱动。
useAnimatedValue
Animated API 中的可动画值,Animated.timing/spring/decay 等函数驱动它变化,Animated.View 组件监听变化并更新样式。
Reanimated 3
第三方动画库(软件公司 Software Mansion 维护),所有动画逻辑通过 worklet 在 UI 线程执行,不依赖 JS 线程,是目前 RN 动画的最佳实践。
worklet
标记了 'worklet' 指令的函数,会被 Reanimated 编译为可以在 UI 线程运行的代码。在 worklet 内部不能访问 JS 上下文(如 useState),只能用 useSharedValue 等专用 API。
useSharedValue
Reanimated 的跨线程共享值。可以在 JS 线程和 UI 线程同时读写,变化时自动触发动画,是 Reanimated 动画的驱动核心。
withTiming / withSpring
Reanimated 的动画函数。withTiming 是线性/缓动动画(精确控制时长),withSpring 是弹簧物理动画(自然、有弹性的感觉),在 UI 线程执行,不阻塞 JS。
Gesture Handler
手势识别库(同为 Software Mansion 维护),在原生层识别手势事件,避免通过 Bridge 传递带来的延迟。与 Reanimated 配合可实现丝滑的拖拽、捏合、滑动等交互。
共享元素动画(Shared Element Transition)
页面切换时,某个元素(如图片缩略图)流畅地"变形"到目标页面的对应位置。React Navigation 和 Reanimated 配合可实现此效果,大幅提升页面跳转的视觉质量。
JS 驱动 vs UI 线程动画
JS 驱动动画(旧方式,会卡顿)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JS Thread (16ms/帧)
┌──────────┬──────────┬──────────┬─────────┐
│ 计算动画值 │ JS业务逻辑 │ 计算动画值 │ 掉帧! │
└──────────┴──────────┴──────────┴─────────┘
│ Bridge序列化 │ 忙碌,无法计算
▼
UI Thread
┌──────────┬──────────┬──────────┬─────────┐
│ 更新样式 │ 等待... │ 更新样式 │ 等待... │ ← 掉帧
└──────────┴──────────┴──────────┴─────────┘
UI 线程动画(Reanimated worklet)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JS Thread UI Thread (独立,始终流畅)
┌─────────────────┐ ┌────┬────┬────┬────┬────┐
│ JS 业务逻辑 │ │帧1 │帧2 │帧3 │帧4 │帧5 │ ← 60fps
│ (可以很忙) │ │动画│动画│动画│动画│动画│
└─────────────────┘ └────┴────┴────┴────┴────┘
│ ▲
│ SharedValue.value = x (跨线程共享)
└─────────────────────┘
Animated API(内置方案)
import { Animated, Easing } from 'react-native';
import { useRef, useEffect } from 'react';
export function FadeInCard({ children }: { children: React.ReactNode }) {
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(20)).current;
useEffect(() => {
// 并行执行两个动画
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();
}, []);
return (
<Animated.View
style={{
opacity,
transform: [{ translateY }],
}}
>
{children}
</Animated.View>
);
}
Reanimated 3:UI 线程动画
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export function DraggableCard() {
// SharedValue 可跨 JS 和 UI 线程共享
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
// 拖拽手势(在 UI 线程处理,无延迟)
const gesture = Gesture.Pan()
.onBegin(() => {
'worklet'; // 在 UI 线程执行
scale.value = withSpring(1.05); // 按下时放大
})
.onUpdate((event) => {
'worklet';
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
'worklet';
// 松手后弹回原位(弹簧动画)
translateX.value = withSpring(0, { damping: 20, stiffness: 200 });
translateY.value = withSpring(0, { damping: 20, stiffness: 200 });
scale.value = withSpring(1);
});
// 动画样式(worklet,在 UI 线程计算)
const animatedStyle = useAnimatedStyle(() => {
// 根据拖拽距离计算旋转角度
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 gesture={gesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>拖拽我!</Text>
</Animated.View>
</GestureDetector>
);
}
useNativeDriver 规则
使用 Animated API 时,
useNativeDriver: true 只支持 transform(translate/scale/rotate)和 opacity 属性的原生驱动。不支持 width、height、backgroundColor 等布局属性。如果需要动画这些属性,必须用 Reanimated 3。
Reanimated 安装注意
Reanimated 3 需要在
babel.config.js 中添加插件:plugins: ['react-native-reanimated/plugin'],且必须放在所有插件的最后。修改 babel 配置后需要清除缓存重启(npx expo start --clear)。