React Native动态旋转秀:Animated打造三张图片炫彩效果
在移动端开发中,流畅的动画能够大大提升用户体验和界面活力。本文将带你用 React Native 的 Animated
API,打造一个由三张图片组成的炫彩旋转秀。通过不同的旋转速度、延迟与交错,我们可以轻松实现视觉冲击力十足的动态效果,让你的页面瞬间“活”起来。
本文内容包含:
- 动画效果预览与思路概览
- 环境准备与依赖说明
- Animated 核心原理解析
- 完整示例代码(分步讲解)
- 图解布局与动画流程
- 常见疑问与扩展思路
一、动画效果预览与思路概览
1.1 效果预览
以下用文字和 ASCII 图示简单模拟效果(实际效果请运行示例代码查看):
┌──────────────────────────────────────────────────────────────┐
│ │
│ ① 图片A:缓慢顺时针旋转 ② 图片B:中速逆时针旋转 ③ 图片C:快速顺时针旋转 │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Image A │ │ Image B │ │ Image C │ │
│ │ (慢速) │ │ (中速) │ │ (快速) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ ↻ ↺ ↻ │
│ │
└──────────────────────────────────────────────────────────────┘
- Image A:以最慢速度顺时针旋转,每圈时长约 8 秒。
- Image B:以中等速度逆时针旋转,每圈时长约 5 秒。
- Image C:以最快速度顺时针旋转,每圈时长约 3 秒。
三张图片同步开始,但可以通过延迟或不同速度,制造有节奏的视觉效果。你也可以在代码中自行调整速度、延迟、方向、图片素材等,生成完全个性化的旋转秀。
TIP:下文我们使用三张示例图片,你可以替换成任意本地资产或网络图片,如头像、Logo、插画等。
1.2 实现思路
使用
Animated.Value
创建“旋转角度”动态值- 每个图片对应一个
Animated.Value(0)
,代表初始角度为 0。 - 借助
Animated.timing
或Animated.loop
不断更新该值,实现“无限旋转”。
- 每个图片对应一个
通过
interpolate
将数值映射到角度(deg)- JS 层的
Animated.Value
是一个数字,我们要把它映射到字符串形式的"0deg" → "360deg"
。 - 通过
spin.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] })
,让数值 0→1 对应角度 0°→360°。
- JS 层的
在每个
Animated.Image
上,通过style={{ transform: [{ rotate: spin }] }}
应用旋转rotate
接受一个字符串(如"45deg"
),配合 interpolation,就会呈现旋转动画。
给不同图片设置不同的动画时长(duration)与方向(正向/反向),甚至延迟(delay)
- 顺时针:
toValue = 1
且插值为"0deg"
→"360deg"
。 - 逆时针:可以把
outputRange
写成["360deg", "0deg"]
或者将Animated.Value
从 0 → -1。 - 无限循环:使用
Animated.loop
包裹Animated.timing
,并设置useNativeDriver: true
(更流畅)。
- 顺时针:
组合三个动画同时启动
- 可以使用
Animated.parallel
或者在useEffect
中分开start()
。
- 可以使用
下面就按照上述思路,逐步演示从环境准备到完整实现的全过程。
二、环境准备与依赖说明
2.1 React Native 项目环境
本文示例以 React Native 0.63+ 版本为基准(同样适用于 0.64、0.65、0.66 等)。请确保你的开发环境已经安装:
- Node.js ≥ 12
- Yarn 或 npm
- React Native CLI(或使用 Expo,但示例中假设使用原生项目)
- Xcode(macOS)或 Android Studio(若需 Android 兼容可略过)
若你尚未创建 RN 项目,可在终端执行:
npx react-native init AnimatedRotationDemo
cd AnimatedRotationDemo
接着,进入项目目录进行开发。
2.2 引用图片资源
在项目根目录下创建一个 assets
文件夹,将示例图片(imageA.png
、imageB.png
、imageC.png
)放入其中。示例结构:
AnimatedRotationDemo/
├─ android/
├─ ios/
├─ node_modules/
├─ assets/
│ ├─ imageA.png
│ ├─ imageB.png
│ └─ imageC.png
├─ App.js
└─ package.json
提示:示例图片可以任意替换,只要确保路径正确即可。
三、Animated 核心原理解析
在正式编写代码之前,我们先梳理一下 Animated
API 的一些核心概念。
3.1 Animated.Value 与插值 (interpolate)
new Animated.Value(initialValue)
- 创建一个可动画化的数值对象。
initialValue
可以是数字,通常初始设为 0。
interpolate
- 用于将
Animated.Value
在一定区间内映射到其他区间(数值、角度、颜色等)。 例如:
const spinValue = new Animated.Value(0); const spin = spinValue.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
- 这样当
spinValue
从 0 变化到 1 时,spin
会从"0deg"
变化到"360deg"
。
- 用于将
3.2 构建动画:Animated.timing
Animated.timing(animatedValue, config)
- 基于时间(duration)驱动动画,将
animatedValue
从config.fromValue
(或当前值)过渡到config.toValue
。 config
常用参数:toValue
: 目标数值(如1
、-1
)。duration
: 动画持续时长(毫秒)。easing
: 缓动函数(可选,默认线性)。delay
: 延迟启动时长(毫秒,可选)。useNativeDriver
: 是否使用原生驱动(对于 transform、opacity 等属性必须设为true
)。
- 基于时间(duration)驱动动画,将
示例:
Animated.timing(spinValue, {
toValue: 1,
duration: 5000,
useNativeDriver: true,
}).start();
3.3 无限循环:Animated.loop
Animated.loop(animation, config)
- 将单次动画包装成一个“无限循环”动画。
config
可设{ iterations: number }
,若省略则无限执行。
例如:
const spinAnimation = Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
})
);
spinAnimation.start();
- 注意:如果要重新开始循环,需在每次循环结束前手动重置
spinValue
,或者在Animated.timing
中使用useNativeDriver
并将toValue: 1
后,在loop
中自动回到初始值。
3.4 同步启动多个动画:Animated.parallel
Animated.parallel(arrayOfAnimations, config)
- 同时启动一组动画。
config
可选{ stopTogether: boolean }
,默认为true
,表示其中一个动画停止时,其它动画也停止。
示例:
Animated.parallel([
Animated.loop(Animated.timing(spinA, { toValue: 1, duration: 8000, useNativeDriver: true })),
Animated.loop(Animated.timing(spinB, { toValue: 1, duration: 5000, useNativeDriver: true })),
Animated.loop(Animated.timing(spinC, { toValue: 1, duration: 3000, useNativeDriver: true })),
]).start();
这样就能够同时让三张图片按照各自速度无限旋转。
四、完整示例代码(分步讲解)
下面给出一个完整的 App.js
,示例会在中心水平布局三张图片,分别以不同速度和方向旋转。之后我们会分段解析每一步。
4.1 完整 App.js
代码
// App.js
import React, { useRef, useEffect } from "react";
import {
View,
StyleSheet,
Animated,
Easing,
Dimensions,
} from "react-native";
const { width } = Dimensions.get("window");
const IMAGE_SIZE = 100; // 每张图片的宽高
export default function App() {
// 1. 创建三个 Animated.Value,用于驱动旋转
const spinValueA = useRef(new Animated.Value(0)).current;
const spinValueB = useRef(new Animated.Value(0)).current;
const spinValueC = useRef(new Animated.Value(0)).current;
// 2. useEffect 中启动动画
useEffect(() => {
// A:顺时针,8 秒一圈
Animated.loop(
Animated.timing(spinValueA, {
toValue: 1,
duration: 8000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
// B:逆时针,5 秒一圈 -> 通过 outputRange 反转
Animated.loop(
Animated.timing(spinValueB, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
// C:顺时针,3 秒一圈,延迟 500ms 启动
Animated.loop(
Animated.timing(spinValueC, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
delay: 500,
useNativeDriver: true,
})
).start();
}, [spinValueA, spinValueB, spinValueC]);
// 3. 使用 interpolate 将 0->1 映射到 '0deg'->'360deg'
const spinA = spinValueA.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"], // 顺时针
});
const spinB = spinValueB.interpolate({
inputRange: [0, 1],
outputRange: ["360deg", "0deg"], // 逆时针
});
const spinC = spinValueC.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"], // 顺时针
});
return (
<View style={styles.container}>
{/* 图片 A */}
<Animated.Image
source={require("./assets/imageA.png")}
style={[
styles.image,
{
transform: [{ rotate: spinA }],
},
]}
/>
{/* 图片 B */}
<Animated.Image
source={require("./assets/imageB.png")}
style={[
styles.image,
{
transform: [{ rotate: spinB }],
marginHorizontal: 20, // 三张图片间距
},
]}
/>
{/* 图片 C */}
<Animated.Image
source={require("./assets/imageC.png")}
style={[
styles.image,
{
transform: [{ rotate: spinC }],
},
]}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
flexDirection: "row", // 水平排列三张图片
backgroundColor: "#F5F5F5",
},
image: {
width: IMAGE_SIZE,
height: IMAGE_SIZE,
borderRadius: 8, // 可选,为图片添加圆角
},
});
4.2 代码解析
4.2.1 创建 Animated.Value
const spinValueA = useRef(new Animated.Value(0)).current;
const spinValueB = useRef(new Animated.Value(0)).current;
const spinValueC = useRef(new Animated.Value(0)).current;
- 使用
useRef
创建三个不同的Animated.Value
,初始值都为0
。 useRef(...).current
可以保证在组件多次渲染时,这三个值不会被重新创建,保持稳定。
4.2.2 启动动画循环
放在 useEffect
中,确保在组件挂载后只执行一次:
useEffect(() => {
// A:顺时针,8 秒一圈
Animated.loop(
Animated.timing(spinValueA, {
toValue: 1,
duration: 8000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
// B:逆时针,5 秒一圈
Animated.loop(
Animated.timing(spinValueB, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
// C:顺时针,3 秒一圈,延迟 500ms
Animated.loop(
Animated.timing(spinValueC, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
delay: 500,
useNativeDriver: true,
})
).start();
}, [spinValueA, spinValueB, spinValueC]);
Animated.loop(Animated.timing(...))
- 将
Animated.timing
包裹在Animated.loop
中,使其无限循环。 - 因为
toValue: 1
,每次循环结束后,Animated.loop
会自动重置spinValueX
到0
并重新开始。
- 将
不同配置
spinValueA
:duration: 8000
,即 8 秒完成 0→1。spinValueB
:duration: 5000
,5 秒一圈。spinValueC
:duration: 3000
,3 秒一圈,并且带delay: 500
,即挂载后先等待 500ms 再开始循环。
4.2.3 插值映射:Numeric → “deg”
const spinA = spinValueA.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"], // 顺时针
});
const spinB = spinValueB.interpolate({
inputRange: [0, 1],
outputRange: ["360deg", "0deg"], // 逆时针
});
const spinC = spinValueC.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"], // 顺时针
});
spinValueX
会从 0 → 1,不断循环。interpolate
将其映射成旋转角度,生成一个新的 Animated 可动画化值(类型为字符串,如"45deg"
)。- 顺时针:
["0deg", "360deg"]
。 - 逆时针:把输出范围倒过来:
["360deg", "0deg"]
。
4.2.4 在 Animated.Image
上应用 transform
<Animated.Image
source={require("./assets/imageA.png")}
style={[
styles.image,
{
transform: [{ rotate: spinA }],
},
]}
/>
Animated.Image
与普通Image
组件唯一不同是它可以接受Animated.Value
类型的样式属性。- 在
style
中,以transform: [{ rotate: spinA }]
应用插值后的旋转角度值,配合useNativeDriver: true
,实现高性能的原生动画。
4.2.5 布局:水平排列三张图片
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
flexDirection: "row", // 水平排列
backgroundColor: "#F5F5F5",
},
image: {
width: IMAGE_SIZE,
height: IMAGE_SIZE,
borderRadius: 8,
},
});
- 容器
flexDirection: "row"
,将三张图片水平排列。 justifyContent: "center"
+alignItems: "center"
,确保内容在屏幕中央。IMAGE_SIZE
(100)可以按需调整。
4.2.6 运行效果
- 项目启动:
npx react-native run-ios
。 - 在模拟器或真机中,你会看到三张图片并排在屏幕中央,它们分别以不同速度与方向不断旋转,形成炫彩效果。
五、图解布局与动画流程
5.1 布局示意图
┌────────────────────────────────────────────────────────────┐
│ │
│ ←——— 左间距 ———— 三张图片 ———— 右间距 ————› │
│ │
│ (center) [ ImageA ] ─── 20px ─── [ ImageB ] ─── 20px ─ [ ImageC ] │
│ │
│ ↑ ↑ ↑ │
│ | | | │
│ transform: transform: transform:│
│ rotate: spinA rotate: spinB rotate: spinC│
│ │
└────────────────────────────────────────────────────────────┘
- ImageA 位于左侧,旋转动画由
spinA
驱动。 - ImageB 位于中间,左右都有 20px 间距,旋转动画由
spinB
逆时针驱动。 - ImageC 位于右侧,旋转动画由
spinC
驱动。
5.2 动画时序流程
Time Axis: 0ms ─────────────────────────────────────────> ∞
ImageA.spinValueA: 0 -> 1 (8 秒) → 重置为 0 → 继续循环
ImageB.spinValueB: 0 -> 1 (5 秒) → 重置为 0 → 继续循环
ImageC.spinValueC: (Delay 500ms) 0 -> 1 (3 秒) → 重置为 0 → 继续循环
┌──────────────────────────────────────────────┐
Time 0ms │ 动画初始化: spinValueA/B/C 设为 0 │
└──────────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
A、B 同步开始 C 延迟 500ms 不旋转
(spinA & spinB)
Time 500ms ┌──────────────────────────────────────────────┐
│ C 启动: spinValueC 从 0 开始旋转 │
└──────────────────────────────────────────────┘
Time 3000ms ┌──────────────────────────────────────────────┐
│ C 完成一圈(0->1),循环重置为 0,继续下一圈 │
└──────────────────────────────────────────────┘
Time 5000ms ┌──────────────────────────────────────────────┐
│ B 完成一圈(0->1),循环重置为 0,继续下一圈 │
└──────────────────────────────────────────────┘
Time 8000ms ┌──────────────────────────────────────────────┐
│ A 完成一圈(0->1),循环重置为 0,继续下一圈 │
└──────────────────────────────────────────────┘
... 无限循环 依此类推 ...
- spinValueA:每 8000ms(8s)完成一次 0→1:对应旋转 0°→360°。
- spinValueB:每 5000ms(5s)完成一次,逆时针。
- spinValueC:延迟 500ms 后,每 3000ms(3s)完成一次。
六、常见疑问与扩展思路
在实际开发中,你可能会对该示例进行各种扩展和优化。以下列举一些常见问题及可延展思路,帮助你更深入理解并灵活运用。
6.1 如何让三张图片“分时”启动动画?
如果想让三张图片依次启动,而不是同时启动,可以在 Animated.loop(Animated.timing)
中为后两者设置更大的 delay
。
Animated.loop(
Animated.timing(spinValueA, {
toValue: 1,
duration: 8000,
easing: Easing.linear,
useNativeDriver: true,
delay: 0,
})
).start();
Animated.loop(
Animated.timing(spinValueB, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
delay: 1000, // 延迟 1 秒后启动
})
).start();
Animated.loop(
Animated.timing(spinValueC, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true,
delay: 2000, // 延迟 2 秒后启动
})
).start();
- 这样会在 A 启动后 1 秒时启动 B,再过 1 秒时启动 C。
- 三张图片的动画时间线更具层次感。
6.2 如何给旋转添加“缩放”或“位移”效果?
你可以在 transform
属性数组中添加更多动画值。例如,让图片在旋转时也做“呼吸”缩放。
// 在 hook 之外,创建缩放值
const scaleValueA = useRef(new Animated.Value(1)).current;
// 在 useEffect 中:
Animated.loop(
Animated.sequence([
Animated.timing(scaleValueA, {
toValue: 1.2,
duration: 2000,
useNativeDriver: true,
easing: Easing.ease,
}),
Animated.timing(scaleValueA, {
toValue: 1.0,
duration: 2000,
useNativeDriver: true,
easing: Easing.ease,
}),
])
).start();
// 然后在 style 中:
transform: [
{ rotate: spinA },
{ scale: scaleValueA },
],
Animated.sequence
将多个动画按序执行。scaleValueA
在 1.0 和 1.2 之间循环,配合Animated.loop
,实现“呼吸”效果。- 组合在
transform
数组中时,顺序就是“先旋转、再缩放”。
同理,你可以加入位移动画:
const translateYValueA = useRef(new Animated.Value(0)).current;
// 定义上下位移动画
Animated.loop(
Animated.sequence([
Animated.timing(translateYValueA, {
toValue: -10,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(translateYValueA, {
toValue: 10,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(translateYValueA, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
])
).start();
// Style:
transform: [
{ rotate: spinA },
{ translateY: translateYValueA },
],
- 这样会让图片在旋转的同时,上下浮动 10 个像素,提升动态感。
6.3 为什么需要设置 useNativeDriver: true
?
useNativeDriver: true
表示将动画交由原生驱动(NativeDriver)执行,避免 JavaScript 线程的卡顿。- 转换(transform)、透明度(opacity)等样式均可使用原生驱动。
- 如果不设置
useNativeDriver: true
,且动画逻辑较多,会导致 JS 线程阻塞,界面渲染不流畅。
6.4 Animated vs. Reanimated
- 本文示例使用 React Native 自带的
Animated
。如果你对性能有更高要求,可考虑使用 Reanimated 库,它提供了更丰富的原生驱动功能和声明式动画 API。 - Reanimated 在语法上与 Animated 略有不同,但思路相似,配置“旋转 + 缩放”等动画也非常方便。
七、总结
本文以“React Native动态旋转秀”为主题,详细展示了如何使用 Animated
API 打造三张图片不同速度、方向的无限旋转动画,并对 Animated 的核心原理、常用方法(Animated.Value
、interpolate
、Animated.timing
、Animated.loop
、Animated.parallel
)进行了深入解析。我们提供了:
- 效果预览与思路:先通过 ASCII 图示了解动画效果,再制定实现思路。
- 环境准备:如何在 RN 项目中引用本地图片资源。
- 核心原理解析:Animated.Value 与插值、时序动画、无限循环、并行动画等。
- 完整示例代码:一个可复制粘贴的
App.js
,三张图片并排渲染,分别以 8s/5s/3s 周期旋转。 - 代码分步解析:逐行解释如何创建动画驱动值、如何插值映射到角度、如何应用到组件样式。
- 图解布局与时序:用 ASCII 图示说明布局结构与动画时序。
- 扩展与疑问:包括分时启动、缩放/位移动画、NativeDriver 原理、以及使用 Reanimated 的可选方案。
通过本文,你应当能够:
- 熟练使用
Animated
API 实现旋转、缩放、位移动画 - 使用
interpolate
将数值映射到度数、透明度、颜色等多种属性 - 灵活组合
Animated.loop
与Animated.sequence
制作复杂动画 - 在组件挂载时启动并保持无限循环动画,以及实现分时延迟启动
- 掌握动画性能优化要点,合理设置
useNativeDriver