【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶
开篇:让界面"活"起来
前八课的界面大多是静态的。点按钮,内容瞬间切换;打开页面,元素直接出现。能用,但不够自然。
真实的应用需要动画来传达信息:按钮按下时的轻微缩放,让用户知道"我点到了";列表项删除时的淡出,让用户知道"这项没了";页面切换时的滑动,让用户知道"我从哪里来,要到哪里去"。
动画不只是好看,它是用户体验的一部分。它告诉用户:发生了什么,接下来会发生什么。
React Native 提供了多种动画方案。这一课从最基础的 Animated API 讲起,再深入到社区主流的 Reanimated,最后结合 Pressable 和手势,做出真正有交互感的界面。
一、动画的基本思路
1.1 动画的本质
动画的本质,是让某个值随时间变化,然后把变化应用到界面上。
比如透明度从 0 变到 1,位置从左边变到右边,大小从 1 倍变到 1.5 倍。这些变化在时间轴上平滑地发生,用户看到的就是动画。
在 React Native 中,动画值不是普通的 state。用 state 做动画性能很差:
// JavaScript + React Native
// 不推荐:用 state 做动画
const [opacity, setOpacity] = useState(0); // 创建状态 opacity
setInterval(() => { // 定时器
setOpacity((prev) => Math.min(prev + 0.02, 1)); // 每 16 毫秒增加一点
}, 16); // 间隔 16 毫秒
每 16 毫秒更新一次 state,触发一次重新渲染。这种方式性能很差,而且不够平滑。
1.2 两套互补的动画系统
React Native 内置了两套互补的动画系统:
- Animated:对特定值进行细粒度、交互式控制;
- LayoutAnimation:用于全局布局事务的动画。
这一课重点讲 Animated,同时介绍社区主流的 Reanimated。
二、Animated API 入门
2.1 Animated.Value
Animated 的核心是 Animated.Value——一个可以被动画驱动的值。
// JavaScript + React Native
import { useRef } from 'react'; // 引入 useRef
import { Animated, View } from 'react-native'; // 引入 Animated 和 View
export default function App() { // 定义并默认导出 App
const fadeAnim = useRef(new Animated.Value(0)).current; // 创建动画值,初始 0
return ( // 返回界面描述
<View> // 外层容器
<Animated.View // 可动画的 View
style={{ // 样式
opacity: fadeAnim, // 透明度由动画值决定
width: 100, // 宽度 100
height: 100, // 高度 100
backgroundColor: 'blue', // 蓝色背景
}} // 结束样式
/>
</View> // 结束容器
); // 结束 return
} // 结束 App
Animated.Value(0) 创建了一个初始值为 0 的动画值。它被连接到 Animated.View 的 opacity 上。当这个值变化时,透明度会跟着变化。
注意:Animated.Value 应该用 useRef 保存,不要每次渲染都重新创建。
2.2 Animated.timing:基于时间的动画
Animated.timing 是最常用的动画类型,它让值在指定时间内从当前值变化到目标值。
// JavaScript + React Native
import { useRef } from 'react'; // 引入 useRef
import { Animated, View, Button, StyleSheet } from 'react-native'; // 引入组件
export default function App() { // 定义并默认导出 App
const fadeAnim = useRef(new Animated.Value(0)).current; // 创建动画值,初始 0
const fadeIn = () => { // 定义淡入函数
Animated.timing(fadeAnim, { // 使用 timing 动画
toValue: 1, // 目标值 1
duration: 1000, // 时长 1000 毫秒
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动动画
}; // 结束 fadeIn
const fadeOut = () => { // 定义淡出函数
Animated.timing(fadeAnim, { // 使用 timing 动画
toValue: 0, // 目标值 0
duration: 1000, // 时长 1000 毫秒
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动动画
}; // 结束 fadeOut
return ( // 返回界面描述
<View style={styles.container}> // 居中容器
<Animated.View // 可动画的 View
style={[styles.box, { opacity: fadeAnim }]} // 透明度由动画值决定
/>
<Button title="淡入" onPress={fadeIn} /> // 淡入按钮
<Button title="淡出" onPress={fadeOut} /> // 淡出按钮
</View> // 结束容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }, // 居中容器样式
box: { width: 100, height: 100, backgroundColor: 'blue', marginBottom: 20 }, // 方块样式
}); // 结束样式表
关键参数:
toValue:目标值;duration:动画时长(毫秒);useNativeDriver:是否使用原生驱动。
2.3 useNativeDriver 的重要性
useNativeDriver: true 让动画在原生线程执行,不经过 JavaScript 线程。这样即使 JavaScript 在忙,动画也能保持流畅。
但不是所有属性都支持原生驱动。目前只支持:
opacity;transform(包括translateX、translateY、scale、rotate等)。
像 width、height、backgroundColor 这些属性不支持原生驱动。如果要对这些属性做动画,只能用 useNativeDriver: false,但性能会差一些。
2.4 Animated.spring:基于物理的动画
Animated.spring 使用弹簧物理模型,动画更自然:
// JavaScript + React Native
Animated.spring(scaleAnim, { // 弹簧动画
toValue: 1.5, // 目标值 1.5
friction: 3, // 摩擦力
tension: 40, // 张力
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动动画
friction:摩擦力,值越大越不容易弹;tension:张力,值越大弹得越快。
弹簧动画适合做"按下缩放"“弹出”"回弹"等效果。
2.5 组合动画
多个动画可以组合执行:
// JavaScript + React Native
// 顺序执行:先淡入,再右移
Animated.sequence([ // 顺序组合
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }), // 先淡入
Animated.timing(translateX, { toValue: 100, duration: 500, useNativeDriver: true }), // 再右移
]).start(); // 启动
// 并行执行:同时淡入和右移
Animated.parallel([ // 并行组合
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }), // 淡入
Animated.timing(translateX, { toValue: 100, duration: 500, useNativeDriver: true }), // 右移
]).start(); // 启动
2.6 插值
Animated.Value 可以通过 interpolate 映射到不同的输出范围:
// JavaScript + React Native
const rotate = fadeAnim.interpolate({ // 插值
inputRange: [0, 1], // 输入范围
outputRange: ['0deg', '360deg'], // 输出范围
}); // 结束插值
<Animated.View style={{ transform: [{ rotate }] }} /> // 应用旋转
当 fadeAnim 从 0 变到 1 时,旋转角度从 0 度变到 360 度。插值让一个动画值可以驱动多个属性。
2.7 Animated 的局限
Animated API 能满足基础需求,但有几个局限:
- 原生驱动只支持有限的属性;
- 手势驱动的动画需要经过 Bridge 通信,可能卡顿;
- 复杂的动态动画写起来比较受限。
对于更复杂的场景,社区提供了 Reanimated。
三、Reanimated:更强大的动画方案
3.1 Reanimated 是什么
react-native-reanimated 是目前 React Native 社区最主流的动画库。它的核心优势是:动画在 UI 线程执行,不经过 JavaScript 线程。
React Native 有两个线程:JS 线程和 UI 线程。普通的动画计算在 JS 线程执行,如果 JS 在忙(比如处理大量数据),动画就会卡顿。Reanimated 通过"worklet"把动画计算搬到 UI 线程,即使 JS 很忙,动画也能保持流畅。
3.2 安装
Reanimated v4 需要 React Native New Architecture(0.76 或更高版本)。
# Bash 终端命令
npx expo install react-native-reanimated react-native-worklets # 安装 Reanimated 和 worklets
然后在 babel.config.js 中添加插件:
// JavaScript
module.exports = { // 导出配置
presets: ['module:metro-react-native-babel-preset'], // 预设
plugins: [
'react-native-worklets/plugin', // worklets 插件,必须放在最后
],
};
清除缓存并重新构建:
# Bash 终端命令
npm start -- --reset-cache # 清除缓存启动
npx expo prebuild # 预构建
3.3 useSharedValue:共享值
useSharedValue 是 Reanimated 的核心。它创建的值可以在 JS 线程和 UI 线程之间共享,不需要通信开销。
// JavaScript + React Native
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated'; // 引入组件
export default function App() { // 定义并默认导出 App
const opacity = useSharedValue(0); // 创建共享值,初始 0
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
opacity: opacity.value, // 透明度由共享值决定
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<Animated.View // 可动画的 View
style={[{ width: 100, height: 100, backgroundColor: 'blue' }, animatedStyle]} // 样式
/>
); // 结束 return
} // 结束 App
和 Animated.Value 不同,useSharedValue 创建的值通过 .value 访问,而且可以直接在 worklet 中读写。
3.4 useAnimatedStyle:动画样式
useAnimatedStyle 接收一个 worklet 函数,返回样式对象。当依赖的共享值变化时,样式自动更新,不需要重新渲染组件。
// JavaScript + React Native
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ scale: scale.value }], // 缩放由共享值决定
opacity: opacity.value, // 透明度由共享值决定
})); // 结束 useAnimatedStyle
3.5 动画函数速查
| 函数 | 作用 | 适用场景 |
|---|---|---|
withTiming | 基于时间 | 匀速、变速动画 |
withSpring | 弹簧物理 | 自然回弹、按下反馈 |
withDecay | 衰减 | 惯性滑动 |
withSequence | 顺序执行 | 组合动画 |
withRepeat | 重复执行 | 循环旋转、呼吸效果 |
withTiming:
// JavaScript + React Native
opacity.value = withTiming(1, { duration: 1000 }); // 1000 毫秒内到 1
withSpring:
// JavaScript + React Native
scale.value = withSpring(1.5, { damping: 10, stiffness: 100 }); // 弹簧到 1.5
withDecay:
// JavaScript + React Native
translateX.value = withDecay({ velocity: 1000, deceleration: 0.997 }); // 惯性滑动
withSequence:
// JavaScript + React Native
scale.value = withSequence( // 顺序执行
withTiming(1.2, { duration: 100 }), // 先放大到 1.2
withSpring(1) // 再弹回 1
); // 结束
withRepeat:
// JavaScript + React Native
rotation.value = withRepeat(withTiming(360, { duration: 2000 }), -1, true); // 无限循环旋转
3.6 一个完整的缩放动画
// JavaScript + React Native
import { Pressable, StyleSheet } from 'react-native'; // 引入 Pressable 和 StyleSheet
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; // 引入 Reanimated
export default function App() { // 定义并默认导出 App
const scale = useSharedValue(1); // 创建共享值,初始 1
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ scale: scale.value }], // 缩放由共享值决定
})); // 结束 useAnimatedStyle
const handlePressIn = () => { // 定义按下处理
scale.value = withSpring(0.9, { damping: 10, stiffness: 300 }); // 弹簧到 0.9
}; // 结束 handlePressIn
const handlePressOut = () => { // 定义松开处理
scale.value = withSpring(1, { damping: 10, stiffness: 300 }); // 弹簧回 1
}; // 结束 handlePressOut
return ( // 返回界面描述
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}> // 可点击容器
<Animated.View style={[styles.box, animatedStyle]}> // 可动画的 View
{/* 内容 */}
</Animated.View> // 结束 Animated.View
</Pressable> // 结束 Pressable
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
box: { // 方块样式
width: 120, // 宽 120
height: 120, // 高 120
backgroundColor: '#4A90D9', // 蓝色背景
borderRadius: 16, // 圆角 16
justifyContent: 'center', // 垂直居中
alignItems: 'center', // 水平居中
}, // 结束方块样式
}); // 结束样式表
按下时缩小到 0.9 倍,松开时弹回 1 倍。这就是 Reanimated 最典型的用法:共享值 + 动画函数 + useAnimatedStyle。
四、Pressable 进阶
4.1 点击生命周期
Pressable 提供了完整的点击状态检测:
onPressIn:手指按下时触发;onPressOut:手指抬起时触发;onPress:手指抬起且没有长按时触发;onLongPress:按住超过 500 毫秒时触发。
// JavaScript + React Native (JSX)
<Pressable // 可点击容器
onPressIn={() => console.log('按下')} // 按下时打印
onPressOut={() => console.log('抬起')} // 抬起时打印
onPress={() => console.log('点击')} // 点击时打印
onLongPress={() => console.log('长按')} // 长按时打印
> // 结束开始标签
<Text>点击我</Text> // 文本
</Pressable> // 结束 Pressable
4.2 用 style 函数实现按下反馈
Pressable 的 style 可以是一个函数,接收 { pressed } 状态:
// JavaScript + React Native (JSX)
<Pressable // 可点击容器
style={({ pressed }) => [ // 样式函数
styles.button, // 基础样式
pressed && styles.buttonPressed, // 按下时叠加样式
]} // 结束样式函数
onPress={handlePress} // 点击回调
> // 结束开始标签
<Text>点击我</Text> // 文本
</Pressable> // 结束 Pressable
// JavaScript + React Native
const styles = StyleSheet.create({ // 创建样式表
button: { padding: 16, backgroundColor: '#4A90D9', borderRadius: 8 }, // 按钮样式
buttonPressed: { opacity: 0.7 }, // 按下时透明度降低
}); // 结束样式表
这种方式不需要 useState,按下时自动应用 buttonPressed 样式,松开后恢复。
4.3 android_ripple
Android 平台上,android_ripple 可以启用水波纹效果:
// JavaScript + React Native (JSX)
<Pressable // 可点击容器
android_ripple={{ color: '#ccc', borderless: false }} // Android 水波纹
style={styles.button} // 应用样式
onPress={handlePress} // 点击回调
> // 结束开始标签
<Text>点击我</Text> // 文本
</Pressable> // 结束 Pressable
iOS 上可以用 style 函数的 pressed 状态来实现类似的按下效果。
4.4 hitSlop
手指不够精确,用户可能点到按钮边缘。hitSlop 可以扩大点击热区:
// JavaScript + React Native (JSX)
<Pressable hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> // 扩大热区
<Text>小按钮</Text> // 文本
</Pressable> // 结束 Pressable
4.5 用 Reanimated 做按下缩放
把 Pressable 和 Reanimated 结合,做更丰富的按下效果:
// JavaScript + React Native
import { Pressable, StyleSheet, Text } from 'react-native'; // 引入组件
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; // 引入 Reanimated
export default function AnimatedButton({ title, onPress }) { // 定义动画按钮组件
const scale = useSharedValue(1); // 创建共享值,初始 1
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ scale: scale.value }], // 缩放由共享值决定
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<Pressable // 可点击容器
onPressIn={() => { // 按下时
scale.value = withSpring(0.92, { damping: 15, stiffness: 400 }); // 弹簧到 0.92
}} // 结束按下处理
onPressOut={() => { // 松开时
scale.value = withSpring(1, { damping: 15, stiffness: 400 }); // 弹簧回 1
}} // 结束松开处理
onPress={onPress} // 点击回调
> // 结束开始标签
<Animated.View style={[styles.button, animatedStyle]}> // 可动画的 View
<Text style={styles.text}>{title}</Text> // 按钮文字
</Animated.View> // 结束 Animated.View
</Pressable> // 结束 Pressable
); // 结束 return
} // 结束 AnimatedButton
const styles = StyleSheet.create({ // 创建样式表
button: { // 按钮样式
paddingVertical: 14, // 垂直内边距 14
paddingHorizontal: 32, // 水平内边距 32
backgroundColor: '#4A90D9', // 蓝色背景
borderRadius: 12, // 圆角 12
alignItems: 'center', // 水平居中
}, // 结束按钮样式
text: { color: '#fff', fontSize: 16, fontWeight: 'bold' }, // 文字样式
}); // 结束样式表
按下时轻微缩小,松开时弹回。这种反馈让按钮感觉更"跟手"。
五、手势:Gesture Handler 基础
5.1 为什么需要手势库
React Native 内置的 PanResponder 能处理手势,但性能有限。它在 JS 线程处理每个移动事件,JS 忙的时候手势会卡顿。
react-native-gesture-handler 是社区主流的手势库。它在原生线程识别手势,配合 Reanimated 的共享值,手势和动画都能保持流畅。
5.2 安装
# Bash 终端命令
npx expo install react-native-gesture-handler # 安装手势库
在应用的最外层包裹 GestureHandlerRootView:
// JavaScript + React Native
import { GestureHandlerRootView } from 'react-native-gesture-handler'; // 引入根容器
export default function App() { // 定义并默认导出 App
return ( // 返回界面描述
<GestureHandlerRootView style={{ flex: 1 }}> // 手势根容器
{/* 应用内容 */}
</GestureHandlerRootView> // 结束根容器
); // 结束 return
} // 结束 App
5.3 拖拽示例
// JavaScript + React Native
import { StyleSheet } from 'react-native'; // 引入 StyleSheet
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; // 引入手势组件
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; // 引入 Reanimated
export default function App() { // 定义并默认导出 App
const translateX = useSharedValue(0); // 水平偏移共享值
const translateY = useSharedValue(0); // 垂直偏移共享值
const pan = Gesture.Pan() // 创建拖拽手势
.onUpdate((e) => { // 拖拽过程中
translateX.value = e.translationX; // 更新水平偏移
translateY.value = e.translationY; // 更新垂直偏移
}) // 结束 onUpdate
.onEnd(() => { // 拖拽结束
translateX.value = withSpring(0); // 弹回水平原点
translateY.value = withSpring(0); // 弹回垂直原点
}); // 结束 onEnd
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
transform: [
{ translateX: translateX.value }, // 水平偏移
{ translateY: translateY.value }, // 垂直偏移
],
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<GestureHandlerRootView style={styles.container}> // 手势根容器
<GestureDetector gesture={pan}> // 手势检测器
<Animated.View style={[styles.box, animatedStyle]} /> // 可拖拽的方块
</GestureDetector> // 结束检测器
</GestureHandlerRootView> // 结束根容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, justifyContent: 'center', alignItems: 'center' }, // 居中容器样式
box: { width: 100, height: 100, backgroundColor: '#4A90D9', borderRadius: 16 }, // 方块样式
}); // 结束样式表
Gesture.Pan() 创建了一个拖拽手势。onUpdate 中把手指的移动距离赋给共享值,onEnd 中让方块弹回原位。
5.4 点击手势
// JavaScript + React Native
const tap = Gesture.Tap() // 创建点击手势
.numberOfTaps(2) // 需要双击
.onEnd(() => { // 手势结束时
scale.value = withSpring(scale.value === 1 ? 1.5 : 1); // 切换大小
}); // 结束 onEnd
双击切换大小。numberOfTaps(2) 指定需要双击。
5.5 组合手势
// JavaScript + React Native
const composed = Gesture.Simultaneous(pan, tap); // 同时识别拖拽和点击
Gesture.Simultaneous 让多个手势同时识别。其他组合方式还有 Gesture.Race(竞争)和 Gesture.Exclusive(互斥)。
六、布局动画
6.1 LayoutAnimation
LayoutAnimation 是 React Native 内置的布局动画 API。它在下次布局时自动把视图动画到新位置。
// JavaScript + React Native
import { LayoutAnimation, UIManager, Platform } from 'react-native'; // 引入组件
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { // Android 需要开启
UIManager.setLayoutAnimationEnabledExperimental(true); // 开启实验性布局动画
} // 结束判断
const addItem = () => { // 定义添加函数
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // 配置下一个动画
setItems([...items, newItem]); // 更新状态
}; // 结束 addItem
适合的场景:列表项增删、展开折叠、布局切换。
6.2 Reanimated 的布局动画
Reanimated 提供了更强大的布局动画:
// JavaScript + React Native (JSX)
import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated'; // 引入动画
{items.map((item) => ( // 遍历列表
<Animated.View // 可动画的 View
key={item.id} // 唯一 key
entering={FadeIn} // 进入动画
exiting={FadeOut} // 退出动画
layout={Layout.springify()} // 布局变化动画
> // 结束开始标签
<Text>{item.title}</Text> // 显示标题
</Animated.View> // 结束 Animated.View
))} // 结束 map
entering:元素出现时的动画;exiting:元素消失时的动画;layout:布局变化时的动画。
这比 LayoutAnimation 更灵活,而且运行在 UI 线程。
七、完整示例:可拖拽的卡片
把这一课的知识组合起来,做一个可拖拽的卡片。
// JavaScript + React Native
import { StyleSheet, Text } from 'react-native'; // 引入组件
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; // 引入手势
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; // 引入 Reanimated
export default function App() { // 定义并默认导出 App
const translateX = useSharedValue(0); // 水平偏移
const translateY = useSharedValue(0); // 垂直偏移
const scale = useSharedValue(1); // 缩放
const pan = Gesture.Pan() // 创建拖拽手势
.onBegin(() => { // 手势开始时
scale.value = withSpring(1.05); // 放大到 1.05
}) // 结束 onBegin
.onUpdate((e) => { // 拖拽过程中
translateX.value = e.translationX; // 更新水平偏移
translateY.value = e.translationY; // 更新垂直偏移
}) // 结束 onUpdate
.onEnd(() => { // 拖拽结束
scale.value = withSpring(1); // 缩放回 1
translateX.value = withSpring(0); // 弹回水平原点
translateY.value = withSpring(0); // 弹回垂直原点
}); // 结束 onEnd
const animatedStyle = useAnimatedStyle(() => ({ // 定义动画样式
transform: [
{ translateX: translateX.value }, // 水平偏移
{ translateY: translateY.value }, // 垂直偏移
{ scale: scale.value }, // 缩放
],
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<GestureHandlerRootView style={styles.container}> // 手势根容器
<GestureDetector gesture={pan}> // 手势检测器
<Animated.View style={[styles.card, animatedStyle]}> // 可拖拽的卡片
<Text style={styles.title}>拖拽我</Text> // 标题
<Text style={styles.subtitle}>松手后弹回原位</Text> // 副标题
</Animated.View> // 结束 Animated.View
</GestureDetector> // 结束检测器
</GestureHandlerRootView> // 结束根容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5' }, // 容器
card: { // 卡片样式
width: 200, // 宽 200
height: 200, // 高 200
backgroundColor: '#fff', // 白色背景
borderRadius: 20, // 圆角 20
justifyContent: 'center', // 垂直居中
alignItems: 'center', // 水平居中
shadowColor: '#000', // 阴影颜色
shadowOffset: { width: 0, height: 4 }, // 阴影偏移
shadowOpacity: 0.15, // 阴影透明度
shadowRadius: 12, // 阴影模糊半径
elevation: 8, // Android 阴影
}, // 结束卡片样式
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 4 }, // 标题样式
subtitle: { fontSize: 14, color: '#888' }, // 副标题样式
}); // 结束样式表
卡片可以拖拽,拖拽时轻微放大,松手后弹回原位。手势和动画都在 UI 线程执行,非常流畅。
八、动画与"视图 = 函数(状态)"
动画也是"视图 = 函数(状态)“的一部分,只是"状态"换成了"动画值”:
// 概念表达
动画视图 = 函数(动画值) // 动画视图由动画值决定
Animated.Value或共享值是动画状态;- 动画驱动这些值随时间变化;
- 样式根据动画值计算;
- 值变,样式变,界面更新。
和普通状态不同,动画值的更新不触发组件重新渲染。Animated.View 和 useAnimatedStyle 直接在原生侧更新样式,跳过渲染过程。这就是动画流畅的原因。
九、练习题
练习题1:选择题——Animated 和 Reanimated 的区别
题目:Reanimated 相比 Animated API 的主要优势是什么?
A. 语法更简单
B. 动画在 UI 线程执行,不经过 JS 线程
C. 不需要安装
D. 只支持 iOS
参考答案:B
解读:Reanimated 通过 worklet 把动画计算搬到 UI 线程,即使 JS 线程繁忙,动画也能保持流畅。这是它最核心的优势。
练习题2:填空题——创建动画值
题目:补全代码,创建一个初始值为 0 的 Animated.Value。
// JavaScript + React Native
const fadeAnim = useRef(new Animated.Value(____)).current; // 补全初始值
参考答案:0
解读:Animated.Value(0) 创建初始值为 0 的动画值。用 useRef 保存,避免每次渲染重新创建。
练习题3:改错题——useNativeDriver
题目:下面代码想对 width 使用原生驱动,但有问题。请指出。
// JavaScript + React Native
Animated.timing(widthAnim, { // timing 动画
toValue: 200, // 目标 200
duration: 500, // 时长 500
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动
参考答案:width 不支持原生驱动。应该把 useNativeDriver 设为 false,或者改用 transform: [{ scaleX }] 来实现宽度变化效果。
解读:原生驱动只支持 opacity 和 transform 相关属性。其他属性需要 useNativeDriver: false,性能会差一些。
练习题4:代码阅读题——插值
题目:阅读代码,说出 fadeAnim 从 0 变到 1 时,旋转角度如何变化。
// JavaScript + React Native
const rotate = fadeAnim.interpolate({ // 插值
inputRange: [0, 1], // 输入范围
outputRange: ['0deg', '360deg'], // 输出范围
}); // 结束插值
参考答案:旋转角度从 0 度线性变化到 360 度。
解读:interpolate 把输入范围 [0, 1] 映射到输出范围 ['0deg', '360deg']。
练习题5:动手题——淡入动画
题目:写一个淡入动画,让一个 View 在 800 毫秒内从透明度 0 变为 1。
参考答案:
// JavaScript + React Native
const fadeAnim = useRef(new Animated.Value(0)).current; // 创建动画值
const fadeIn = () => { // 定义淡入函数
Animated.timing(fadeAnim, { // timing 动画
toValue: 1, // 目标 1
duration: 800, // 时长 800
useNativeDriver: true, // 使用原生驱动
}).start(); // 启动
}; // 结束 fadeIn
<Animated.View style={{ opacity: fadeAnim }} /> // 应用动画
解读:Animated.timing 驱动 fadeAnim 从 0 到 1,useNativeDriver: true 让动画在原生线程执行。
练习题6:简答题——useSharedValue 和 Animated.Value 的区别
题目:useSharedValue 和 Animated.Value 有什么区别?
参考答案:useSharedValue 创建的值可以在 JS 线程和 UI 线程之间共享,通过 .value 读写,配合 worklet 使用。Animated.Value 是 Animated API 的值,通过 Animated.timing 等驱动,连接到 Animated.View 的样式属性。
解读:useSharedValue 是 Reanimated 的核心,支持在 UI 线程读写,不需要通信开销。Animated.Value 是 React Native 内置的动画值。
练习题7:Reanimated 基础题
题目:用 useSharedValue 和 withTiming 让一个方块的透明度从 0 变到 1。
参考答案:
// JavaScript + React Native
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; // 引入 Reanimated
const opacity = useSharedValue(0); // 创建共享值,初始 0
const style = useAnimatedStyle(() => ({ // 定义动画样式
opacity: opacity.value, // 透明度由共享值决定
})); // 结束 useAnimatedStyle
opacity.value = withTiming(1, { duration: 1000 }); // 触发动画
<Animated.View style={[styles.box, style]} /> // 应用动画
解读:useSharedValue 创建共享值,useAnimatedStyle 把它映射到样式,withTiming 驱动动画。
练习题8:Pressable 生命周期题
题目:Pressable 的 onPressIn、onPressOut、onPress、onLongPress 分别在什么时候触发?
参考答案:onPressIn 手指按下时触发;onPressOut 手指抬起时触发;onPress 手指抬起且没有长按时触发;onLongPress 按住超过 500 毫秒时触发。
解读:这些回调让你可以精确控制点击的每个阶段,实现丰富的交互反馈。
练习题9:Pressable 样式反馈题
题目:用 Pressable 的 style 函数实现按下时透明度变为 0.7。
参考答案:
// JavaScript + React Native (JSX)
<Pressable // 可点击容器
style={({ pressed }) => [ // 样式函数
{ padding: 16, backgroundColor: '#4A90D9', borderRadius: 8 }, // 基础样式
pressed && { opacity: 0.7 }, // 按下时透明度降低
]} // 结束样式函数
> // 结束开始标签
<Text>点击我</Text> // 文本
</Pressable> // 结束 Pressable
解读:style 函数接收 { pressed },根据按下状态返回不同样式。不需要 useState,按下时自动应用,松开后恢复。
练习题10:withSpring 题
题目:用 withSpring 实现按下缩放到 0.9,松开弹回 1。
参考答案:
// JavaScript + React Native
const scale = useSharedValue(1); // 创建共享值
const style = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ scale: scale.value }], // 缩放由共享值决定
})); // 结束 useAnimatedStyle
// 按下
scale.value = withSpring(0.9, { damping: 15, stiffness: 400 }); // 弹簧到 0.9
// 松开
scale.value = withSpring(1, { damping: 15, stiffness: 400 }); // 弹簧回 1
解读:withSpring 让动画有弹簧效果,比 withTiming 更自然。damping 和 stiffness 控制弹簧的阻尼和刚度。
练习题11:手势基础题
题目:用 Gesture.Pan() 实现拖拽,并在松手后弹回原位。
参考答案:
// JavaScript + React Native
const translateX = useSharedValue(0); // 水平偏移
const translateY = useSharedValue(0); // 垂直偏移
const pan = Gesture.Pan() // 创建拖拽手势
.onUpdate((e) => { // 拖拽过程中
translateX.value = e.translationX; // 更新水平偏移
translateY.value = e.translationY; // 更新垂直偏移
}) // 结束 onUpdate
.onEnd(() => { // 拖拽结束
translateX.value = withSpring(0); // 弹回水平原点
translateY.value = withSpring(0); // 弹回垂直原点
}); // 结束 onEnd
const style = useAnimatedStyle(() => ({ // 定义动画样式
transform: [
{ translateX: translateX.value }, // 水平偏移
{ translateY: translateY.value }, // 垂直偏移
],
})); // 结束 useAnimatedStyle
解读:onUpdate 中把手指移动距离赋给共享值,onEnd 中用 withSpring 弹回原位。
练习题12:布局动画题
题目:用 LayoutAnimation 让列表项添加时有淡入动画。
参考答案:
// JavaScript + React Native
import { LayoutAnimation } from 'react-native'; // 引入 LayoutAnimation
const addItem = () => { // 定义添加函数
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // 配置下一个动画
setItems([...items, newItem]); // 更新状态
}; // 结束 addItem
解读:在更新状态之前调用 configureNext,下一次布局变化会自动添加动画。
练习题13:Reanimated 布局动画题
题目:用 Reanimated 的 FadeIn 和 FadeOut 让列表项出现和消失时有动画。
参考答案:
// JavaScript + React Native (JSX)
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; // 引入动画
{items.map((item) => ( // 遍历列表
<Animated.View // 可动画的 View
key={item.id} // 唯一 key
entering={FadeIn} // 进入动画
exiting={FadeOut} // 退出动画
> // 结束开始标签
<Text>{item.title}</Text> // 显示标题
</Animated.View> // 结束 Animated.View
))} // 结束 map
解读:entering 和 exiting 是 Reanimated 的布局动画属性,比 LayoutAnimation 更灵活,而且运行在 UI 线程。
练习题14:综合题——动画按钮
题目:创建一个按钮,按下时缩放并改变透明度,松开时恢复。
参考答案:
// JavaScript + React Native
import { Pressable, StyleSheet, Text } from 'react-native'; // 引入组件
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; // 引入 Reanimated
export default function AnimatedButton({ title, onPress }) { // 定义动画按钮
const scale = useSharedValue(1); // 缩放共享值
const opacity = useSharedValue(1); // 透明度共享值
const style = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ scale: scale.value }], // 缩放
opacity: opacity.value, // 透明度
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<Pressable // 可点击容器
onPressIn={() => { // 按下时
scale.value = withSpring(0.92, { damping: 15, stiffness: 400 }); // 缩放
opacity.value = withSpring(0.8); // 降低透明度
}} // 结束按下处理
onPressOut={() => { // 松开时
scale.value = withSpring(1, { damping: 15, stiffness: 400 }); // 恢复缩放
opacity.value = withSpring(1); // 恢复透明度
}} // 结束松开处理
onPress={onPress} // 点击回调
> // 结束开始标签
<Animated.View style={[styles.button, style]}> // 可动画的 View
<Text style={styles.text}>{title}</Text> // 按钮文字
</Animated.View> // 结束 Animated.View
</Pressable> // 结束 Pressable
); // 结束 return
} // 结束 AnimatedButton
const styles = StyleSheet.create({ // 创建样式表
button: { // 按钮样式
paddingVertical: 14, // 垂直内边距
paddingHorizontal: 32, // 水平内边距
backgroundColor: '#4A90D9', // 蓝色背景
borderRadius: 12, // 圆角 12
alignItems: 'center', // 水平居中
}, // 结束按钮样式
text: { color: '#fff', fontSize: 16, fontWeight: 'bold' }, // 文字样式
}); // 结束样式表
解读:按下时同时缩放和降低透明度,松开时恢复。Pressable 提供按下和松开的事件,Reanimated 提供流畅的动画。
练习题15:综合应用题——可滑动的卡片
题目:创建一个卡片列表,每张卡片可以左右滑动,滑出屏幕后卡片消失。
参考答案:
// JavaScript + React Native
import { StyleSheet, Text, View } from 'react-native'; // 引入组件
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; // 引入手势
import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS, interpolate, Extrapolation } from 'react-native-reanimated'; // 引入 Reanimated
function SwipeCard({ title, onDismiss }) { // 定义可滑动卡片
const translateX = useSharedValue(0); // 水平偏移
const pan = Gesture.Pan() // 创建拖拽手势
.onUpdate((e) => { // 拖拽过程中
translateX.value = e.translationX; // 更新水平偏移
}) // 结束 onUpdate
.onEnd((e) => { // 拖拽结束
if (Math.abs(e.translationX) > 120) { // 滑出超过 120
translateX.value = withSpring(Math.sign(e.translationX) * 500, {}, () => { // 飞出屏幕
runOnJS(onDismiss)(); // 调用 JS 回调
}); // 结束弹簧
} else { // 否则
translateX.value = withSpring(0); // 弹回原位
} // 结束判断
}); // 结束 onEnd
const style = useAnimatedStyle(() => ({ // 定义动画样式
transform: [{ translateX: translateX.value }], // 水平偏移
opacity: interpolate( // 透明度插值
Math.abs(translateX.value), // 输入:偏移绝对值
[0, 200], // 输入范围
[1, 0], // 输出范围
Extrapolation.CLAMP // 超出范围时截断
), // 结束插值
})); // 结束 useAnimatedStyle
return ( // 返回界面描述
<GestureDetector gesture={pan}> // 手势检测器
<Animated.View style={[styles.card, style]}> // 可滑动的卡片
<Text style={styles.title}>{title}</Text> // 显示标题
</Animated.View> // 结束 Animated.View
</GestureDetector> // 结束检测器
); // 结束 return
} // 结束 SwipeCard
export default function App() { // 定义并默认导出 App
const [cards, setCards] = useState([ // 卡片列表状态
{ id: '1', title: '卡片一' }, // 第一张
{ id: '2', title: '卡片二' }, // 第二张
{ id: '3', title: '卡片三' }, // 第三张
]); // 结束 useState
return ( // 返回界面描述
<GestureHandlerRootView style={styles.container}> // 手势根容器
{cards.map((card) => ( // 遍历卡片
<SwipeCard // 每张卡片
key={card.id} // 唯一 key
title={card.title} // 传入标题
onDismiss={() => setCards((prev) => prev.filter((c) => c.id !== card.id))} // 滑出后移除
/>
))} // 结束 map
</GestureHandlerRootView> // 结束根容器
); // 结束 return
} // 结束 App
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5' }, // 容器
card: { // 卡片样式
width: 280, // 宽 280
height: 160, // 高 160
backgroundColor: '#fff', // 白色背景
borderRadius: 20, // 圆角 20
justifyContent: 'center', // 垂直居中
alignItems: 'center', // 水平居中
marginBottom: 16, // 底部间距
shadowColor: '#000', // 阴影颜色
shadowOffset: { width: 0, height: 4 }, // 阴影偏移
shadowOpacity: 0.1, // 阴影透明度
shadowRadius: 8, // 阴影模糊半径
elevation: 5, // Android 阴影
}, // 结束卡片样式
title: { fontSize: 20, fontWeight: 'bold' }, // 标题样式
}); // 结束样式表
解读:这道题综合运用了手势、共享值、动画函数、插值和条件逻辑。卡片可以左右滑动,滑出超过 120 像素后飞出去并消失,否则弹回原位。滑动过程中透明度根据滑动距离变化。runOnJS 用于在 UI 线程的 worklet 中调用 JS 函数。
十、总结
这一课我们学习了 React Native 中动画和手势的核心知识:
- React Native 内置
AnimatedAPI,对特定值进行细粒度控制; Animated.Value是动画值的载体,通过Animated.timing、Animated.spring等驱动;useNativeDriver: true让动画在原生线程执行,但只支持opacity和transform;Animated.sequence和Animated.parallel组合多个动画;interpolate把动画值映射到不同输出范围;- Reanimated 是社区主流动画库,动画在 UI 线程执行;
useSharedValue创建共享值,useAnimatedStyle映射到样式,withTiming/withSpring驱动动画;Pressable提供完整的点击生命周期:onPressIn、onPressOut、onPress、onLongPress;Pressable的style函数接收{ pressed },实现按下反馈;react-native-gesture-handler配合 Reanimated 实现流畅的手势交互;Gesture.Pan()处理拖拽,Gesture.Tap()处理点击,Gesture.Simultaneous组合手势;LayoutAnimation和 Reanimated 的布局动画处理列表增删。
记住一句话:
动画是让"状态变化"变得可见、可感知,它是"视图 = 函数(状态)"在时间维度上的延伸。
第10课预告
下一课,我们进入 【React Native娓娓道来】第10课 数据持久化与本地存储:AsyncStorage、SecureStore 与文件系统。
更多推荐



所有评论(0)