react native 学习 - 用 Animated 实现 loading 动画

1,393 阅读3分钟

RN 的动画是通过函数驱动来进行的, 并不支持 通过 css + transition / animation 的方式实现。 这与我们 web 前端写动画的习惯不一样, 实现起来也没 web 那么方便, 有点 jquery 时代那味了。这次我们来聊聊如何在 RN 中实现一个 loading 动画

期望效果

3/4 的 橙色圆圈, 不停旋转。

loading-v3.gif

功能实现 - 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 个

  1. 0 - 360 deg 旋转, 用到 Animated.timing() 方法, 定义数值从 0 - 1 的过渡, esing 选择线性
  2. 不断循环, 用到 Animated.loop() 方法, 定义此动画为不断循环
  3. 执行 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.rotatestring正常 style 编写
<Animated.View />style.transform.rotatestring, 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…

参考资料