在 React Native 开发中,动画性能一直是开发者关注的重点。传统的 Animated API 在 JS 线程运行,当 JS 线程负载较高时,动画容易出现卡顿。React Native Reanimated 通过将动画逻辑迁移到 UI 线程,实现了接近原生的流畅体验。
本文将从实际应用角度出发,系统介绍 Reanimated 的核心概念和实战技巧,帮助你快速掌握这个强大的动画库。
为什么选择 Reanimated?
技术对比一览
| 特性 | Animated API | Reanimated |
|---|---|---|
| 执行线程 | 仅 JS 线程 | 支持 UI 线程(主线程) |
| 性能表现 | 受 JS 线程阻塞影响 | 高并发下依然流畅,60fps |
| 手势处理 | 需借助第三方实现 | 原生内置手势支持 |
| TypeScript | 类型支持有限 | 丰富、完善的类型定义 |
| 新架构兼容 | Fabric 支持有限 | 完全兼容 Fabric/Turbo |
| 复杂动画 | 配置复杂,扩展性有限 | 支持中断、组合与嵌套 |
| 动画曲线 | 基础动画曲线 | 内置弹性、衰减等多样曲线 |
核心优势
- UI 线程执行:动画逻辑运行于 UI 线程,不受 JS 线程阻塞影响,效果丝滑流畅。
- 手势支持完善:与
react-native-gesture-handler深度集成,交互体验自然流畅。 - 类型友好:具备完善的 TypeScript 类型声明,开发体验优异。
- 兼容新架构:完美支持 Fabric 与 TurboModules,紧跟 React Native 发展。
- 丰富动画曲线:内置弹性、弹跳、衰减等多种缓动曲线,满足多样动画需求。
- 动画中断与组合:灵活支持动画中断、组合,复杂交互轻松实现。
- 生态活跃:官方文档详尽,社区讨论丰富,兼容主流三方库,生态完善。
快速开始
1. 安装依赖
本文依赖的库版本如下:
- react-native(0.83.0)
- react-native-gesture-handler(^2.30.0):高性能手势库,后续会出文章单独介绍。
- react-native-reanimated(^4.2.1):动画主库。
- react-native-worklets(^0.7.2):让 JS 代码能在 UI 线程和 JSI 环境下高效运行,辅助扩展动画效果。
npm install react-native-reanimated react-native-gesture-handler react-native-worklets
2. 配置 Babel
在 babel.config.js 中添加插件:目的是为了能解析 worklets 函数,使其可以再 UI 线程中运行
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin'],
};
3. Native 配置
- Android
无需多余配置,RN会自动把三方库的 Native 功能打包进 apk 包
- iOS
cd ios && pod install
4. 基础示例
下面是一个简单的动画示例,展示透明度和位移的动画效果:
import React from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
export default function BasicAnimation() {
// 声明动画值
const opacity = useSharedValue(0);
const translateX = useSharedValue(0);
// 计算动画样式
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateX: translateX.value }],
}));
const handlePress = () => {
// 使用线性动画修改动画值
opacity.value = withTiming(opacity.value === 0 ? 1 : 0, {
duration: 500,
});
translateX.value = withTiming(translateX.value === 0 ? 100 : 0, {
duration: 500,
});
};
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Animate" onPress={handlePress} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: '#4CAF50',
marginBottom: 20,
},
});
效果如下:
核心概念
1. Shared Values
useSharedValue 用于存储动画状态,是 Reanimated 的核心数据结构:
const scale = useSharedValue(1);
// 读取当前值
console.log(scale.value);
// 修改值并触发动画
scale.value = withTiming(2, { duration: 300 });
2. Animated Styles
useAnimatedStyle 将 shared values 转换为动画样式:
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` },
],
opacity: opacity.value,
}));
3. Worklets
Worklets 是在 UI 线程执行的 JavaScript 函数,通过 'worklet' 指令标记,上面安装的 react-native-worklets/plugin 插件就是为了解析此关键字
const handlePress = () => {
'worklet';
scale.value = withSpring(1.5);
};
4. 动画函数
Reanimated 提供多种动画函数,满足不同场景需求:
// 线性动画
withTiming(targetValue, { duration: 500 })
// 弹簧动画
withSpring(targetValue, {
damping: 10,
stiffness: 100,
})
// 衰减动画
withDecay({ velocity: 1000 })
// 序列动画
withSequence(
withTiming(100, { duration: 200 }),
withDelay(100, withTiming(0, { duration: 200 }))
)
// 重复动画
withRepeat(withTiming(1, { duration: 500 }), -1, true)
实战案例
案例 1:可拖拽卡片
实现类似 Tinder 的卡片拖拽效果,包含拖拽、回弹和飞出动画:
import React from 'react';
import { Text, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
export default function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
const color = useSharedValue('#2196F3');
const gesture = Gesture.Pan()
.onStart(() => {
scale.value = withSpring(1.1);
color.value = withSpring('#FF5722');
})
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
scale.value = withSpring(1);
color.value = withSpring('#2196F3');
if (Math.abs(translateX.value) > SCREEN_WIDTH / 2) {
translateX.value = withSpring(
translateX.value > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH
);
} else {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
backgroundColor: color.value,
}));
return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text style={styles.text}>拖拽我!</Text>
</Animated.View>
</GestureDetector>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
card: {
width: 300,
height: 200,
backgroundColor: '#2196F3',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
text: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
});
效果如下:
案例 2:可展开/收起的列表项
实现常见的展开/收起效果,包含高度动画和箭头旋转:
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
export default function ExpandableItem({
title,
content,
}: {
title?: string;
content?: string;
}) {
const expanded = useSharedValue(false);
const height = useSharedValue(0);
const opacity = useSharedValue(0);
const toggleExpand = () => {
expanded.value = !expanded.value;
if (expanded.value) {
// 展开增加高度和透明度
height.value = withTiming(100, {
duration: 500,
// 使用贝塞尔曲线动画
easing: Easing.bezier(0.4, 0, 0.2, 1),
});
opacity.value = withTiming(1, { duration: 300 });
} else {
// 收起减小高度,透明度变为 0
height.value = withTiming(0, { duration: 300 });
opacity.value = withTiming(0, { duration: 200 });
}
};
// 内容卡片样式
const contentStyle = useAnimatedStyle(() => ({
height: height.value,
opacity: opacity.value,
}));
// 展开箭头样式
const arrowStyle = useAnimatedStyle(() => ({
transform: [
{
rotate: withTiming(expanded.value ? '180deg' : '0deg', {
duration: 300,
}),
},
],
}));
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.header}
onPress={toggleExpand}
activeOpacity={0.7}
>
<Text style={styles.title}>{title}</Text>
<Animated.Text style={[styles.arrow, arrowStyle]}>▼</Animated.Text>
</TouchableOpacity>
<Animated.View style={[styles.content, contentStyle]}>
<Text style={styles.contentText}>{content}</Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
marginHorizontal: 16,
marginVertical: 8,
borderRadius: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
arrow: {
fontSize: 12,
color: '#666',
},
content: {
overflow: 'hidden',
},
contentText: {
padding: 16,
paddingTop: 0,
fontSize: 14,
color: '#666',
lineHeight: 20,
},
});
效果如下:
案例 3:双指缩放图片
实现图片的双指缩放和拖拽功能:
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
export default function PinchToZoom() {
// 定义缩放值
const scale = useSharedValue(1);
// 用于记录每次缩放结束后的缩放值,实现连续缩放。
const savedScale = useSharedValue(1);
// 定义拖拽X值
const translateX = useSharedValue(0);
// 定义拖拽Y值
const translateY = useSharedValue(0);
// 定义双指手势
const pinchGesture = Gesture.Pinch()
.onStart(() => {
// 双指缩放时,保存当前缩放值
savedScale.value = scale.value;
})
.onUpdate(event => {
// 双指缩放时,根据保存的缩放值和当前缩放值计算新的缩放值
scale.value = savedScale.value * event.scale;
})
.onEnd(() => {
// 双指缩放结束时,恢复缩放值
scale.value = withSpring(1);
});
// 定义拖拽手势
const panGesture = Gesture.Pan()
.onUpdate(event => {
// 拖拽时,根据当前拖拽值计算新的拖拽值
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
// 拖拽结束时,恢复拖拽值
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
// 定义同时执行双指缩放和拖拽手势
const composed = Gesture.Simultaneous(pinchGesture, panGesture);
// 定义动画样式
const animatedStyle = useAnimatedStyle(() => ({
// 根据当前拖拽值和缩放值计算新的拖拽值和缩放值
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={composed}>
<Animated.View style={[styles.imageContainer, animatedStyle]}>
<View style={styles.image} />
</Animated.View>
</GestureDetector>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
imageContainer: {
width: SCREEN_WIDTH * 0.8,
height: SCREEN_WIDTH * 0.8,
},
image: {
flex: 1,
backgroundColor: 'orange',
borderRadius: 8,
},
});
效果如下:
性能优化技巧
1. 避免在动画中调用 JS 函数
❌ 不推荐的做法:
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: Math.sin(Date.now()) }],
}));
✅ 推荐的做法:
const time = useSharedValue(0);
useEffect(() => {
const interval = setInterval(() => {
time.value = Date.now();
}, 16);
return () => clearInterval(interval);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: Math.sin(time.value) }],
}));
2. 使用 useDerivedValue 缓存计算
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const combinedTransform = useDerivedValue(() => {
return {
scale: scale.value,
rotation: rotation.value,
};
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: combinedTransform.value.scale },
{ rotate: `${combinedTransform.value.rotation}deg` },
],
}));
3. 减少不必要的重渲染
const AnimatedComponent = React.memo(({ value }) => {
const animatedStyle = useAnimatedStyle(() => ({
opacity: value.value,
}));
return <Animated.View style={animatedStyle} />;
});
4. 使用 runOnJS 调用 JS 函数
const handleAnimationComplete = () => {
console.log('Animation completed!');
};
const animatedStyle = useAnimatedStyle(() => ({
opacity: withTiming(1, { duration: 500 }, (finished) => {
if (finished) {
runOnJS(handleAnimationComplete)();
}
}),
}));
5. 使用 useAnimatedScrollHandler 优化滚动动画
const scrollOffset = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollOffset.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => ({
opacity: 1 - scrollOffset.value / 200,
}));
return (
<Animated.ScrollView
onScroll={scrollHandler}
scrollEventThrottle={16}
>
<Animated.View style={[styles.header, headerStyle]}>
<Text>Header</Text>
</Animated.View>
</Animated.ScrollView>
);
总结
Reanimated 通过 UI 线程执行动画,解决了传统 Animated API 的性能瓶颈。掌握以下核心概念即可快速上手:
- Shared Values - 动画状态管理
- Animated Styles - 样式动画化
- Worklets - UI 线程函数
- Gestures - 手势交互
- 动画函数 - withTiming、withSpring 等
进阶学习方向
- 深入学习
react-native-gesture-handler的高级手势 - 探索 Reanimated 的新特性(Layout Animations、Shared Transitions)
- 学习性能优化最佳实践
- 构建复杂的动画组合效果
参考资源
通过本文的学习,你应该能够使用 Reanimated 构建流畅的动画效果了。建议在实际项目中多加练习,逐步掌握更高级的动画技巧。