RN
的动画是通过函数驱动
来进行的, 并不支持 通过 css + transition / animation
的方式实现。 这与我们 web 前端写动画的习惯不一样, 实现起来也没 web
那么方便, 有点 jquery
时代那味了。这次我们来聊聊如何在 RN
中实现一个 loading 动画
期望效果
3/4 的 橙色圆圈, 不停旋转。
功能实现 - Animated
这里只会对涉及 loading 动画
所用到 的 API
进行介绍, 至于其他,请移步RN官网
第一步 - 初始化动画帧
根据官方提供的演示,我们需要通过 useRef
的形式把 Animated.Value
对象缓存下来, 并设置 0
作为初始值
const App = () => {
const numAni = useRef(new Animated.Value(0)).current
}
第二步 - 定义数值映射
我们希望 loading 动画能进行 无限循环 旋转的操作,这里就用到了 样式中 transform -> rotate
来实现了, 但是 rotate
使用的 单位是 deg
而不是 number
。 一般思路, 我们按道理可以通过 ${numAni}deg
的赋值方式来实现
但 Animated
不允许我们通过动态改变样式的形式来实现动画过渡,以下代码是不被允许且会报错的
const App = () => {
const numAni = useRef(new Animated.Value(0)).current
useEffect(() => {
// 初始化动画
})
// 以下 transform 写法是错误的
return (
<Animated.View
style={{
...styles.demo,
transform: [{ rotate: `${numAni}deg` }]
}}
>
<View style={styles.demo__circle} />
</Animated.View>
)
}
正确的方式是需要通过 Animated.Value
提供的 interpolate
插值方法来进行映射
const App = () => {
// 1. 初始化 动画帧
const numAni = useRef(new Animated.Value(0)).current
// 2. 定义转义
const spin = numAni.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})
// 3. 初始化动画
useEffect(() => {
// 初始化动画
})
// 4. 将spin 赋值到 rotate
return (
<Animated.View
style={{
...styles.demo,
transform: [{ rotate: spin }]
}}
>
<View style={styles.demo__circle} />
</Animated.View>
)
}
第三步 - 定义过度动画
我们的 loading 需要执行的动画有 2 个
- 从
0 - 360 deg 旋转
, 用到Animated.timing()
方法, 定义数值从0 - 1
的过渡,esing
选择线性
不断循环
, 用到Animated.loop()
方法, 定义此动画为不断循环- 执行
start()
播放动画
const App = () => {
// 1. 初始化 动画帧
const numAni = useRef(new Animated.Value(0)).current
// 2. 定义转义
const spin = numAni.interpolate({...})
// 3. 初始化动画
useEffect(() => {
// 配置循环,然后执行 start 执行动画
Animated.loop(
// 配置过度动画
Animated.timing(numAni, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
easing: Easing.linear
})
).start()
})
// 4. 将spin 赋值到 rotate
return (
...
)
}
第四步 - 渲染
动画渲染需要用到 Animated
内置的 View
, Text
等标签如 <Animated.View />
, 而不是 RN
内置的 标签
const App = () => {
// 1. 初始化 动画帧
const numAni = useRef(new Animated.Value(0)).current
// 2. 定义转义
const spin = numAni.interpolate({...})
// 3. 初始化动画
useEffect(() => {
...
})
// 4. 将spin 赋值到 rotate
return (
<Animated.View
style={{
...styles.demo,
transform: [{ rotate: spin }]
}}
>
<View style={styles.demo__circle} />
</Animated.View>
)
}
可以看到,这里的 <Animated.View />
和 <View />
在 style
属性的赋值上是有区别的
标签 | style | 类型 | 说明 |
---|---|---|---|
<View /> | style.transform.rotate | string | 正常 style 编写 |
<Animated.View /> | style.transform.rotate | string , Animated.Value | 如传入 Animated.Value , 即 例子中的 spin , loading 会根据过渡动画的描述来进行相应的变化 |
代码演示
下面是核心代码的完整展示
import { StyleSheet, View, Animated, Easing } from 'react-native'
import { useEffect, useRef, FC } from 'react'
const styles = StyleSheet.create({
demo: {},
demo__circle: {}
})
const App: FC<{}> = () => {
// 初始化动画所需要的变量
const numAni = useRef(new Animated.Value(0)).current
// 通过 Animated.Value 提供的插值 方法,定义出 number 与 角度 deg 之间的映射关系
const spin = numAni.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})
// 初始化函数
useEffect(() => {
// 每次初始化前,都需要重置动画
numAni.resetAnimation()
// 配置循环,然后执行 start 执行动画
Animated.loop(
// 配置过度动画
Animated.timing(numAni, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
easing: Easing.linear
})
).start()
}, [numAni])
// 注意 Animated 需要搭配 他提供的 View, Text 等 标签来使用,
// 不能使用 RN 自带的,否则无效果且报错
return (
<Animated.View
style={{
...styles.demo,
transform: [{ rotate: spin }]
}}
>
<View style={styles.demo__circle} />
</Animated.View>
)
}
demo演示: snack.expo.dev/@jackness12…