react-native-reanimated 实战 - 简单 loading 动画

999 阅读4分钟

这次我们聊聊如何使用 react-native-reanimated, 在 RN 实现一个 loaing 动画, 顺便讲讲 与 RN 提供的 Animated 相比, 有哪些差异

期望效果

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

loading-v3.gif

功能实现

这里只会对涉及 loading 动画 所用到 的 API 进行介绍, 至于其他,请移步 react-native-reanimated 官网

STEP 1 - 初始 动画帧

相比 RNAnimatedreact-native-reanimated 初始化动画帧时,并不需要搭配 useRef 来使用,更加直观简洁

Animated 方式

import { Animated } from 'react-native'
import { useRef } from 'react'

export default function App () {
  // 动画帧初始化, 订阅初始化值 为 0
  const numAni = useRef(new Animated.Value(0)).current
}

react-native-reanimated 方式

import { useSharedValue } from 'react-native-reanimated'

export default function App () {
  // 动画帧初始化, 订阅初始化值 为 0
  const num = useSharedValue(0)
}

STEP 2 - 描述动画轨迹

react-native-reanimated 提供的 useAnimatedStyle 方法, 在里面可以按照我们平时 RNstyle 那样写动画样式, 比 RN Animated 更好理解

Animated 方式

由于 <Animated.View /> 内置的 style.transform.rotate 不支持 deg 单位, 你还需要搭配插值函数 interpolate 进行转义

import { StyleSheet, View, Animated, Easing } from 'react-native'
import { useEffect, useRef, FC } from 'react'

const styles = StyleSheet.create({
  demo: {
    //...
  },
  demo__circle: {
    //...
  }
})

const App: FC<{}> = () => {
  // 1. 动画帧初始化
  const numAni = useRef(new Animated.Value(0)).current
  
  // 2. 单位转换
  const spin = numAni.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg']
  })
  
  // 3. 动画帧描述 并开始执行
  useEffect(() => {
    Animated.loop(
      Animated.timing(numAni, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
        easing: Easing.linear
      })
    ).start()
  }, [numAni])

  // 4. 渲染
  return (
    <Animated.View
      style={{
        ...styles.demo,
        transform: [{ rotate: spin }]
      }}
    >
      <View style={styles.demo__circle} />
    </Animated.View>
  )
}

react-native-reanimated 方式

动画过程所需要改变的样式 直接通过 useAnimatedStyle 来进行定义, 对于习惯写 react 的人来讲,更加好理解,返回的 animateStyle 直接当成正常 style 放到 react-native-reanimated 提供的 <Animated.View />style 里面即可, 这与 RN 的写法保持一致

import { StyleSheet, View } from 'react-native'
import { useEffect } from 'react'
import Animated, {
  cancelAnimation,
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  withRepeat,
  Easing
} from 'react-native-reanimated'

const styles = StyleSheet.create({
  demo: {
    //...
  },
  demo__circle: {
    //...
  }
})

export default function App () {
  // 1. 初始化动画帧
  const num = useSharedValue(0)
  
  // 2. 定义动画过程中需要改变的样式
  const animatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{ rotate: `${num.value}deg` }]
    }
  })
  
  // 3. 描述动画
  useEffect(() => {
    // withRepeat - 定义动画重复次数, -1 为无限
    // withTiming - 定义动画轨迹
    num.value = withRepeat(withTiming(-365, { duration: 1000, easing: Easing.linear }), -1, false)
  }, [num])
  
  // 4. 渲染
  return (
    <Animated.View style={[styles.demo, animatedStyles]}>
      <View style={styles.demo__circle} />
    </Animated.View>
  )
}


STEP 3 - 渲染

RN Animated 一致, 需要搭配 react-native-reanimated 所内置的 <Animated.View /> 等标签使用

Animated 方式

这里的 style 并不能通过 ${spin}deg 来直接赋值, 必须通过 interpolate插值方法来进行转义

import { StyleSheet, View, Animated, Easing } from 'react-native'
import { useEffect, useRef, FC } from 'react'

const styles = StyleSheet.create({
  demo: {
    //...
  },
  demo__circle: {
    //...
  }
})

const App: FC<{}> = () => {
  // 1. 动画帧初始化
  const numAni = useRef(new Animated.Value(0)).current
  
  // 2. 单位转换
  const spin = numAni.interpolate({
    //..
  })
  
  // 3. 动画帧描述 并开始执行
  useEffect(() => {
    //..
  }, [numAni])

  // 4. 渲染
  return (
    <Animated.View
      style={{
        ...styles.demo,
        transform: [{ rotate: spin }]
      }}
    >
      <View style={styles.demo__circle} />
    </Animated.View>
  )
}

react-native-reanimated 方式

把 通过 useAnimatedStyle() 定义的 animatedStyles 直接当成样式赋值到 <Animated.View />style 里面即可, 非常直观

import { StyleSheet, View } from 'react-native'
import { useEffect } from 'react'
import Animated, {
  cancelAnimation,
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  withRepeat,
  Easing
} from 'react-native-reanimated'

const styles = StyleSheet.create({
  demo: {
    //...
  },
  demo__circle: {
    //...
  }
})

export default function App () {
  // 1. 初始化动画帧
  const num = useSharedValue(0)
  
  // 2. 定义动画过程中需要改变的样式
  const animatedStyles = useAnimatedStyle(() => {
    //...
  })
  
  // 3. 描述动画
  useEffect(() => {
    //...
  }, [num])
  
  // 4. 渲染
  return (
    <Animated.View style={[styles.demo, animatedStyles]}>
      <View style={styles.demo__circle} />
    </Animated.View>
  )

STEP BUGFIX - 动画 rerender 异常处理

在动画编写,调试过程中发现, 每当保存代码,触发 expo 推送 手机端进行热更新时,会发现, 动画就变得不正常了

llllll.gif

从表现来讲,感觉应该是 上一次的动画没被销毁, 又一次进行了一次 动画过程描述, 导致 shareValue 多次触发。

查了下官网文档, 看到有个 cancelAnimation 可以处理这个事情, 不过尝试后发现不行, 最后试出,需要再重置一下 shareValue

useEffect(() => {
    // 每次执行前先 cancel 之前的动画
    cancelAnimation(num)
    // shareValue 也需要重置一下
    num.value = 0
    num.value = withRepeat(withTiming(-365, { duration: 1000, easing: Easing.linear }), -1, false)
  }, [num])

代码演示

下面是核心代码(除样式)的完整展示

import { useEffect } from 'react'
import Animated, {
  cancelAnimation,
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  withRepeat,
  Easing
} from 'react-native-reanimated'

const boderColor = '#fac200'
const styles = StyleSheet.create({
  demo: {
    //...
  },
  demo__circle: {
    //...
  }
})

export default function App () {
  const num = useSharedValue(0)
  const animatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{ rotate: `${num.value}deg` }]
    }
  })
  useEffect(() => {
    // 每次执行前先 cancel 之前的动画
    cancelAnimation(num)
    // shareValue 也需要重置一下
    num.value = 0

    num.value = withRepeat(withTiming(-365, { duration: 1000, easing: Easing.linear }), -1, false)
  }, [num])
  return (
    <Animated.View style={[styles.demo, animatedStyles]}>
      <View style={styles.demo__circle} />
    </Animated.View>
  )
}

最后

通过对比,我们可以看到, react-native-reanimated 实现动画会比 内置 Animated 会更加简洁 和 可读性会更加友好一些, 除此之外, 官网也提到在性能上也做到 低延迟多线程 等对 Animated 进行吊打,在这里还是推荐大家来使用的。

当然这里并没有对性能进行更加深入的对比,这里就不作展开了。

参考资料