引言:React Native 动画的新纪元
2026 年,React Native 生态终于迎来了一个让人兴奋的里程碑——Reanimated 4 正式发布稳定版。说实话,这是自 Reanimated 2 引入 Worklet 以来,我觉得最让人期待的一次大版本升级。它把 CSS 动画和过渡效果带进了 React Native 的世界,同时还保持了跟 Worklet 动画系统的完全向后兼容。
这意味着什么呢?简单来说,你现在可以用熟悉的 CSS 动画语法来构建流畅的 60+ FPS 原生动画。而当遇到手势控制、滚动驱动这类复杂场景时,依然可以用强大的 Worklet API。
本文会从核心概念到实战案例,带你全面掌握 Reanimated 4 的动画开发。话不多说,我们开始吧。
安装与环境配置
前置要求
先说个重要的事:Reanimated 4 只支持 React Native 新架构(Fabric)。所以你得确保项目用的是 React Native 0.76 或更高版本。如果还在旧架构上,就需要先完成迁移,或者暂时继续用 Reanimated 3.x。
Expo 项目安装
npx expo install react-native-reanimated react-native-worklets
Expo 项目比较省心——Reanimated 的 Babel 插件已经在 babel-preset-expo 里自动配好了,不需要额外操作。
React Native CLI 项目安装
npm install react-native-reanimated react-native-worklets
cd ios && pod install && cd ..
装完之后,需要去 babel.config.js 里把插件从 'react-native-reanimated/plugin' 改成 'react-native-worklets/plugin':
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// ... 其他插件
'react-native-worklets/plugin', // 必须放在最后
],
};
划重点:Worklet 插件必须放在 plugins 数组的最后一个位置。这点千万别忘了,否则 Worklet 代码没法正确处理,debug 起来会很头疼。
架构变更:Worklet 独立化
Reanimated 4 有个值得关注的架构变更——Worklet 被抽到了独立的 react-native-worklets 包里。这种模块化设计挺聪明的,意味着其他库(不只是 Reanimated)也能用上 Worklet 的能力,对整个 React Native 生态来说是件好事。
核心概念详解
SharedValue(共享值)
共享值是 Reanimated 动画系统的地基。它们是同时存在于 JavaScript 线程和 UI 线程的特殊变量——你在 JS 端更新数据,UI 线程直接读取来刷新界面,中间没有任何桥接通信的开销。
import { useSharedValue } from 'react-native-reanimated';
function MyComponent() {
const offset = useSharedValue(0);
const isActive = useSharedValue(false);
// 通过 .value 属性读写
const handlePress = () => {
offset.value = 100;
isActive.value = true;
};
// 使用 React Compiler 时,推荐使用 get/set 方法
const handlePressWithCompiler = () => {
offset.set(100);
isActive.set(true);
console.log(offset.get()); // 100
};
return /* ... */;
}
注意:如果你的项目用了 React Compiler,就别直接访问 .value 属性了,改用 .get() 和 .set() 方法来确保兼容性。这个坑我见不少人踩过。
useAnimatedStyle(动画样式)
useAnimatedStyle 是连接共享值和组件样式的桥梁。它接受一个回调函数,共享值一变,样式就自动重算——整个过程都在 UI 线程跑,完全不阻塞 JS 线程。
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';
export default function AnimatedBox() {
const width = useSharedValue(100);
const animatedStyle = useAnimatedStyle(() => ({
width: withTiming(width.value, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
}),
}));
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button
title="随机宽度"
onPress={() => {
width.value = Math.random() * 300 + 50;
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
box: { height: 80, backgroundColor: '#6C63FF', borderRadius: 12 },
});
关键点:动画样式必须用在 Animated 命名空间下的组件上(比如 Animated.View、Animated.Text),普通的 View 上用了等于没用。这是新手最容易犯的错误之一。
Worklet(工作线程函数)
Worklet 是标记了 'worklet' 的函数,它们直接在 UI 线程而不是 JS 主线程上运行。这也是 Reanimated 能实现丝滑动画的关键——就算 JS 线程忙得不可开交,动画照样不卡。
import { runOnJS } from 'react-native-reanimated';
// Worklet 函数示例
function clampValue(value, min, max) {
'worklet';
return Math.min(Math.max(value, min), max);
}
// 在 Worklet 中调用 JS 线程函数
function updateState(newValue) {
// 这是一个普通 JS 函数
setState(newValue);
}
function onGestureUpdate(value) {
'worklet';
const clamped = clampValue(value, 0, 100);
// 需要通过 runOnJS 回到 JS 线程
runOnJS(updateState)(clamped);
}
Worklet 本质上是闭包,能访问外部作用域的变量。不过这里有个性能陷阱需要注意:只有 Worklet 里实际引用的变量才会被捕获。千万别在 Worklet 里捕获大型对象,不然性能会很糟糕。正确的做法是先把需要的属性提出来:
// ❌ 不推荐:捕获整个对象
const config = { threshold: 50, maxSpeed: 200, /* ... 更多属性 */ };
const worklet = () => {
'worklet';
return config.threshold; // 整个 config 对象都被捕获了
};
// ✅ 推荐:只捕获需要的值
const threshold = config.threshold;
const worklet = () => {
'worklet';
return threshold; // 只捕获一个数字
};
Reanimated 4 新特性:CSS 动画与过渡
这是 Reanimated 4 最重磅的新功能。Software Mansion 团队一直在琢磨一件事——怎么给最常见的那类动画(状态变化触发的动画)提供一个又简洁又好用的声明式 API。最终他们选了 CSS 动画和过渡标准,说实话这个决定很明智。毕竟 CSS 动画是经过了无数 Web 项目实战验证的规范,Web 开发者基本零学习成本就能上手。
CSS 过渡效果(Transitions)
CSS 过渡提供了一种非常简洁的声明式方式来处理状态变化驱动的动画。你只需要告诉它"哪些属性要动画"和"怎么动",然后更新状态就行——剩下的全交给 Reanimated。
import Animated from 'react-native-reanimated';
import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
export default function TransitionDemo() {
const [expanded, setExpanded] = useState(false);
return (
<Pressable onPress={() => setExpanded(!expanded)}>
<Animated.View
style={{
width: expanded ? 300 : 150,
height: expanded ? 200 : 80,
backgroundColor: expanded ? '#6C63FF' : '#A29BFE',
borderRadius: expanded ? 24 : 12,
// CSS 过渡属性
transitionProperty: ['width', 'height', 'backgroundColor', 'borderRadius'],
transitionDuration: 400,
transitionTimingFunction: 'ease-in-out',
}}
>
<Text style={styles.text}>
{expanded ? '点击收起' : '点击展开'}
</Text>
</Animated.View>
</Pressable>
);
}
支持的过渡属性有这些:
transitionProperty— 指定哪些样式属性要做动画transitionDuration— 过渡持续时间(毫秒)transitionDelay— 延迟多久开始过渡(毫秒)transitionTimingFunction— 缓动函数,比如'ease'、'ease-in-out'、'linear'transitionBehavior— 过渡行为控制
跟传统 Worklet 写法比起来,CSS 过渡的好处是代码量大幅减少。不用创建 SharedValue,不用写 useAnimatedStyle 回调,直接在 style 里声明过渡规则就完事了。对于简单的状态切换动画来说,体验提升非常明显。
CSS 关键帧动画(Keyframe Animations)
过渡处理的是从状态 A 到状态 B 的变化。但如果你想做多步骤的动画序列呢?那就得用关键帧动画了。你可以精确控制动画时间线上每个节点的样式——有点像在做动画分镜。
import Animated from 'react-native-reanimated';
// 脉冲动画关键帧
const pulse = {
from: {
transform: [{ scale: 1 }],
opacity: 1,
},
'50%': {
transform: [{ scale: 1.15 }],
opacity: 0.8,
},
to: {
transform: [{ scale: 1 }],
opacity: 1,
},
};
// 旋转加淡入动画
const spinFadeIn = {
'0%': {
transform: [{ rotate: '0deg' }, { scale: 0.5 }],
opacity: 0,
},
'70%': {
transform: [{ rotate: '300deg' }, { scale: 1.1 }],
opacity: 0.9,
},
'100%': {
transform: [{ rotate: '360deg' }, { scale: 1 }],
opacity: 1,
},
};
export default function CSSKeyframeDemo() {
return (
<Animated.View
style={{
width: 120,
height: 120,
backgroundColor: '#FF6B6B',
borderRadius: 60,
// CSS 关键帧动画属性
animationName: pulse,
animationDuration: 2000,
animationIterationCount: 'infinite',
animationTimingFunction: 'ease-in-out',
}}
/>
);
}
关键帧动画支持的属性:
animationName— 关键帧定义对象animationDuration— 单次动画时长(毫秒)animationDelay— 开始前的延迟(毫秒)animationIterationCount— 重复次数,或者'infinite'无限循环animationDirection— 播放方向('normal'、'reverse'、'alternate')animationFillMode— 填充模式('forwards'、'backwards'、'both')animationTimingFunction— 缓动函数
CSS 动画预设库
Software Mansion 还贴心地提供了 react-native-css-animations 预设库,里面有一大堆现成的动画效果可以直接用:
npm install react-native-css-animations
import { bounce, fadeIn, shakeX } from 'react-native-css-animations';
// 直接应用预设动画
<Animated.View
style={{
animationName: bounce,
animationDuration: 1000,
animationIterationCount: 'infinite',
}}
/>
对于一些常见的动画效果(弹跳、抖动、淡入淡出之类的),用预设库能省不少时间。
布局动画:进场与退场
布局动画解决的是一个很实际的问题:元素被添加到界面或从界面移除时,怎么让它优雅地出现和消失?Reanimated 提供了丰富的预定义动画,当然你也可以用 Keyframe 来自定义。
预定义进场/退场动画
import Animated, {
FadeIn,
FadeOut,
SlideInLeft,
SlideOutRight,
BounceIn,
ZoomIn,
FlipInXUp,
Layout,
} from 'react-native-reanimated';
import { useState } from 'react';
import { Button, View, StyleSheet } from 'react-native';
export default function LayoutAnimationDemo() {
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
setItems([...items, Date.now()]);
};
const removeItem = (id) => {
setItems(items.filter((item) => item !== id));
};
return (
<View style={styles.container}>
<Button title="添加项目" onPress={addItem} />
{items.map((item) => (
<Animated.View
key={item}
entering={SlideInLeft.duration(400).springify()}
exiting={SlideOutRight.duration(300)}
layout={Layout.springify()}
style={styles.item}
>
<Pressable onPress={() => removeItem(item)}>
<Text>项目 {item} (点击删除)</Text>
</Pressable>
</Animated.View>
))}
</View>
);
}
自定义修饰链
所有预定义动画都支持链式调用来做自定义。这个 API 设计得挺舒服的:
// 自定义进场动画
FadeIn
.delay(200) // 延迟 200ms
.duration(600) // 持续 600ms
.springify() // 使用弹簧物理效果
.damping(12) // 弹簧阻尼
.withCallback((finished) => {
'worklet';
console.log('动画完成:', finished);
});
Keyframe 进场/退场动画
当预定义动画不够用的时候(总有那种设计师给的天马行空的动效需求吧),可以用 Keyframe 类来定义完全自定义的进退场效果:
import { Keyframe, Easing } from 'react-native-reanimated';
const customEntering = new Keyframe({
0: {
opacity: 0,
transform: [{ translateY: -50 }, { scale: 0.8 }, { rotate: '-10deg' }],
},
60: {
opacity: 0.8,
transform: [{ translateY: 10 }, { scale: 1.05 }, { rotate: '2deg' }],
easing: Easing.out(Easing.quad),
},
100: {
opacity: 1,
transform: [{ translateY: 0 }, { scale: 1 }, { rotate: '0deg' }],
},
}).duration(800);
const customExiting = new Keyframe({
from: {
opacity: 1,
transform: [{ translateX: 0 }, { scale: 1 }],
},
to: {
opacity: 0,
transform: [{ translateX: 200 }, { scale: 0.5 }],
},
}).duration(500);
// 使用
<Animated.View entering={customEntering} exiting={customExiting}>
{/* 内容 */}
</Animated.View>
小提醒:如果要动画 transform 属性,确保所有关键帧里 transform 数组内的变换属性顺序要一致。不然你会遇到一些很诡异的动画表现。
手势驱动动画
手势交互可以说是移动端动画最核心的场景了。Reanimated 4 跟 react-native-gesture-handler 的集成非常深入——手势回调自动在 UI 线程跑,响应速度几乎零延迟。
拖拽与弹回效果
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { StyleSheet } from 'react-native';
export default function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
const panGesture = Gesture.Pan()
.onStart(() => {
scale.value = withSpring(1.1);
})
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
scale.value = withSpring(1);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, animatedStyle]}>
{/* 卡片内容 */}
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
card: {
width: 200,
height: 280,
backgroundColor: '#6C63FF',
borderRadius: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
});
实战案例:可滑动底部面板
底部面板(Bottom Sheet)几乎是每个移动应用都会用到的交互模式。下面这个例子展示了怎么用手势和 Worklet 做一个可拖拽、带吸附点的底部面板——我个人觉得这是 Reanimated 手势动画最典型的应用场景之一:
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
import { Dimensions, StyleSheet, View } from 'react-native';
const SCREEN_HEIGHT = Dimensions.get('window').height;
const SNAP_POINTS = {
COLLAPSED: SCREEN_HEIGHT - 100,
HALF: SCREEN_HEIGHT * 0.5,
EXPANDED: 80,
};
export default function BottomSheet({ children }) {
const translateY = useSharedValue(SNAP_POINTS.COLLAPSED);
const startY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => {
startY.value = translateY.value;
})
.onUpdate((event) => {
translateY.value = Math.max(
SNAP_POINTS.EXPANDED,
startY.value + event.translationY
);
})
.onEnd((event) => {
const velocity = event.velocityY;
const currentY = translateY.value;
// 根据速度和位置决定吸附到哪个点
let snapPoint;
if (velocity > 500) {
// 快速下滑 → 收起
snapPoint = SNAP_POINTS.COLLAPSED;
} else if (velocity < -500) {
// 快速上滑 → 展开
snapPoint = SNAP_POINTS.EXPANDED;
} else if (currentY < SCREEN_HEIGHT * 0.35) {
snapPoint = SNAP_POINTS.EXPANDED;
} else if (currentY < SCREEN_HEIGHT * 0.65) {
snapPoint = SNAP_POINTS.HALF;
} else {
snapPoint = SNAP_POINTS.COLLAPSED;
}
translateY.value = withSpring(snapPoint, {
damping: 20,
stiffness: 150,
mass: 0.8,
});
});
const sheetStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
const backdropStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateY.value,
[SNAP_POINTS.EXPANDED, SNAP_POINTS.COLLAPSED],
[0.5, 0],
Extrapolation.CLAMP
),
}));
return (
<>
<Animated.View style={[styles.backdrop, backdropStyle]} />
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.sheet, sheetStyle]}>
<View style={styles.handle} />
{children}
</Animated.View>
</GestureDetector>
</>
);
}
const styles = StyleSheet.create({
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#000',
},
sheet: {
position: 'absolute',
left: 0,
right: 0,
height: SCREEN_HEIGHT,
backgroundColor: '#fff',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.15,
shadowRadius: 12,
elevation: 16,
paddingHorizontal: 20,
},
handle: {
width: 40,
height: 5,
backgroundColor: '#DDD',
borderRadius: 3,
alignSelf: 'center',
marginTop: 10,
marginBottom: 20,
},
});
手势组合:双击缩放
实际开发中经常需要同时处理多种手势。比如一个图片查看器,既要支持双指缩放,又要支持双击放大,还要能拖拽平移。Reanimated 4 和 Gesture Handler 配合起来处理这种场景毫不费力:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
export default function ZoomableImage() {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onUpdate((event) => {
scale.value = savedScale.value * event.scale;
})
.onEnd(() => {
if (scale.value < 1) {
scale.value = withSpring(1);
savedScale.value = 1;
} else {
savedScale.value = scale.value;
}
});
const doubleTap = Gesture.Tap()
.numberOfTaps(2)
.onEnd(() => {
if (scale.value > 1) {
scale.value = withTiming(1);
savedScale.value = 1;
translateX.value = withTiming(0);
translateY.value = withTiming(0);
} else {
scale.value = withTiming(2.5);
savedScale.value = 2.5;
}
});
const panGesture = Gesture.Pan()
.onUpdate((event) => {
if (scale.value > 1) {
translateX.value = event.translationX;
translateY.value = event.translationY;
}
})
.onEnd(() => {
if (scale.value <= 1) {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
}
});
const composed = Gesture.Simultaneous(
pinchGesture,
Gesture.Race(doubleTap, panGesture)
);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<GestureDetector gesture={composed}>
<Animated.Image
source={{ uri: 'https://example.com/photo.jpg' }}
style={[{ width: '100%', height: 400 }, animatedStyle]}
resizeMode="contain"
/>
</GestureDetector>
);
}
CSS 动画 vs Worklet:到底怎么选?
有了两种动画方案之后,很多人会纠结该用哪个。其实选择起来并不复杂,关键看你的具体场景。
用 CSS 动画/过渡就够了的场景
- 状态驱动的 UI 变化 — 展开/收起、显示/隐藏、主题切换这类
- Loading 动画 — 旋转、脉冲、闪烁等循环效果
- 按钮交互反馈 — 悬停、按下状态的视觉变化
- 简单的入场动画 — 淡入、滑入等一次性效果
- 任何已知起止状态的动画 — 你清楚地知道从 A 到 B 要怎么变
该用 Worklet 的场景
- 手势驱动动画 — 拖拽、缩放、旋转这些需要逐帧控制的交互
- 滚动驱动动画 — 视差效果、吸顶动画等基于滚动位置的效果
- 屏幕转场动画 — 页面间的共享元素过渡
- 多动画编排 — 需要从一个动画值派生出多个其他动画
- 物理模拟 — 弹簧、衰减等需要实时计算的效果
混合使用示例
实际项目中,最聪明的做法是混合使用。比如一个卡片组件里,背景色切换用 CSS 过渡(简单高效),拖拽交互用 Worklet(精确控制):
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
export default function HybridCard() {
const [isActive, setIsActive] = useState(false);
const translateX = useSharedValue(0);
// 拖拽使用 Worklet
const pan = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX;
})
.onEnd(() => {
translateX.value = withSpring(0);
});
const dragStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View
style={[
dragStyle,
{
padding: 20,
borderRadius: 16,
// 背景色变化使用 CSS 过渡
backgroundColor: isActive ? '#6C63FF' : '#E8E6FF',
transitionProperty: ['backgroundColor'],
transitionDuration: 300,
},
]}
>
<Pressable onPress={() => setIsActive(!isActive)}>
<Text>{isActive ? '已激活' : '未激活'}</Text>
</Pressable>
</Animated.View>
</GestureDetector>
);
}
性能优化最佳实践
Reanimated 4 的 CSS 声明式动画在性能方面有天然优势——框架能更好地理解你的动画意图,从而做出更优的调度。但还是有几个点值得注意。
1. 布局动画构建器要在组件外部定义
// ✅ 推荐:在组件外部定义
const enterAnimation = SlideInLeft.duration(300).springify();
function ListItem({ item }) {
return (
<Animated.View entering={enterAnimation}>
{/* ... */}
</Animated.View>
);
}
// ❌ 避免:在组件内部每次渲染都重新创建
function ListItem({ item }) {
return (
<Animated.View entering={SlideInLeft.duration(300).springify()}>
{/* ... */}
</Animated.View>
);
}
这个看起来是小事,但在长列表里影响可不小。每次渲染都重新创建动画对象,积少成多开销很可观。
2. 避免不必要的 SharedValue 频繁更新
// ✅ 推荐:使用 withSpring/withTiming 包装
offset.value = withSpring(targetValue);
// ❌ 避免:在高频事件中直接设置(如果不需要实时跟踪的话)
onScroll={(e) => {
offset.value = e.nativeEvent.contentOffset.y; // 滚动监听中这样做是合理的
}}
3. CSS 过渡属性要精确指定
// ✅ 推荐:只过渡你需要的属性
transitionProperty: ['backgroundColor', 'borderRadius']
// ❌ 避免:过渡所有属性
transitionProperty: 'all'
用 'all' 虽然写起来方便,但性能开销更大。养成指定具体属性的习惯吧。
4. 记得用 cancelAnimation 做清理
import { cancelAnimation } from 'react-native-reanimated';
import { useEffect } from 'react';
function MyComponent() {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 2000 });
return () => {
// 组件卸载时取消动画
cancelAnimation(progress);
};
}, []);
}
忘记取消动画是个常见的内存泄漏来源,尤其是在频繁挂载/卸载的组件上。
从 Reanimated 3 迁移
好消息:Reanimated 4 的向后兼容性做得很不错。你现有的 SharedValue、useAnimatedStyle 和 Worklet 代码基本不用改就能跑。不过还是有几个迁移步骤要走:
- 安装
react-native-worklets— 这是 v4 新增的依赖 - 更新 Babel 插件 — 把
'react-native-reanimated/plugin'换成'react-native-worklets/plugin' - 确认新架构已启用 — v4 不再支持旧架构了
- 检查弃用 API — v4 移除了部分 v3 里标记为弃用的 API
- 逐步引入 CSS 动画 — 按自己的节奏在新代码里开始用,不需要一口气重写所有动画
如果你的应用还没迁移到新架构,建议先留在 Reanimated 3.x。别着急,先搞定架构迁移再升 v4 也不迟。
实战案例:动画通知提示(Toast)组件
最后,来个完整的实战。我们用 Reanimated 4 的 CSS 动画和布局动画来做一个带动画效果的 Toast 通知组件——这在实际项目里用得非常多:
import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated';
import { useState, useCallback, useRef } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
const TOAST_TYPES = {
success: { bg: '#10B981', icon: '✓' },
error: { bg: '#EF4444', icon: '✕' },
info: { bg: '#3B82F6', icon: 'ℹ' },
};
function Toast({ id, message, type, onDismiss }) {
const config = TOAST_TYPES[type];
return (
<Animated.View
entering={FadeIn.duration(300).springify().damping(15)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
style={[styles.toast, { backgroundColor: config.bg }]}
>
<Text style={styles.icon}>{config.icon}</Text>
<Text style={styles.message}>{message}</Text>
<Pressable
onPress={() => onDismiss(id)}
style={({ pressed }) => ({
opacity: pressed ? 0.6 : 1,
})}
>
<Text style={styles.dismiss}>关闭</Text>
</Pressable>
</Animated.View>
);
}
export default function ToastContainer() {
const [toasts, setToasts] = useState([]);
const nextId = useRef(0);
const addToast = useCallback((message, type = 'info') => {
const id = nextId.current++;
setToasts((prev) => [...prev, { id, message, type }]);
// 3 秒后自动消失
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const dismissToast = useCallback((id) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<View style={styles.container}>
{toasts.map((toast) => (
<Toast
key={toast.id}
{...toast}
onDismiss={dismissToast}
/>
))}
{/* 测试按钮 */}
<View style={styles.buttons}>
<Pressable onPress={() => addToast('操作成功!', 'success')}>
<Text>成功提示</Text>
</Pressable>
<Pressable onPress={() => addToast('发生错误', 'error')}>
<Text>错误提示</Text>
</Pressable>
<Pressable onPress={() => addToast('这是一条信息', 'info')}>
<Text>信息提示</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
zIndex: 1000,
},
toast: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 12,
marginBottom: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 6,
elevation: 4,
},
icon: { fontSize: 18, color: '#fff', marginRight: 12 },
message: { flex: 1, color: '#fff', fontSize: 15 },
dismiss: { color: '#fff', opacity: 0.8, fontWeight: '600' },
buttons: { marginTop: 20, gap: 10 },
});
总结
Reanimated 4 确实是一次让人眼前一亮的更新。CSS 动画和过渡 API 大幅降低了动画开发的门槛——对于大多数常见的 UI 动画,几行声明式的代码就搞定了,不再需要手动管理 SharedValue 和 useAnimatedStyle 那套流程。
与此同时,Worklet 动画系统依然完整且强大。手势交互、滚动效果、复杂动画编排,这些场景下 Worklet 的能力不可替代。
两种方式能在同一个项目里自由混搭,这才是 Reanimated 4 设计哲学的精髓。
我的建议是:下次遇到动画需求,先试试 CSS 过渡。它更简洁、更好维护,性能也很棒。只有当需求确实超出了 CSS 动画的能力范围,再考虑切到 Worklet。这样的组合策略,足以让你在 React Native 里做出媲美原生的动画体验。