react-native-reanimated 实战 – 有亿点复杂的加载动画

众所周知, RN 在做动画时并没有像 web 那般 通过 样式中提供的 animation, keyframe, transiton 即可满足一般需求。今天我们聊聊一个有些复杂的 加载动画 的实现过程。

阅读本文章,你可能收获以下知识点:

  • Animated.createAnimatedComponent() 创建动画组件
  • interpolate() 插值函数
  • useAnimateProps()
  • useAnimatedStyle()

效果预览

这是一个 yy 这边通用的加载动画

react-native-reanimated 实战 - 有亿点复杂的加载动画

今天我们就聊聊中间的这个动画

react-native-reanimated 实战 - 有亿点复杂的加载动画

可以看到 这个加载动画并不是单纯的旋转,而是补充了一些动画效果的, 看了下 web 的实现方式,其实复杂程度还好。 通过 2 层 animation 实现的

web 实现方式

这里是圈圈的相关代码

html 部分

<span class="spin">
  <!-- + 第一层 animation: rotator -->
  <svg class="spin__svg" width="100%" height="100%" viewBox="0 0 66 66">
    <!-- + 第二层 animation: spinner-dash -->
    <circle
      class="spin__circle"
      fill="none"
      stroke-width="6"
      stroke-linecap="round"
      cx="33"
      cy="33"
      r="30"
    ></circle>
    <!-- - 第二层 animation: spinner-dash -->
  </svg>
  <!-- - 第一层 animation: rotator -->
</span>

css 部分

.spin {
  display: block;
  width: 200px;
  height: 200px;
}
.spin__svg {
  animation: rotator 1.4s linear infinite;
}
.spin__circle {
  stroke-dasharray: 187;
  stroke-dashoffset: 46.75;
  stroke: #fac200;
  transform-origin: center;
  animation: spinner-dash 1.4s ease-in-out infinite;
}
@keyframes rotator {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
@keyframes spinner-dash {
  0% { stroke-dashoffset: 187; }
  50% {
      stroke-dashoffset: 46.75;
      transform: rotate(90deg);
  }
  100% {
      stroke-dashoffset: 187;
      transform: rotate(360deg);
  }
}

演示地址:这里

如何实现

下面介绍下实现的过程

STEP1 – 创建动画组件 AnimatedCircle

RN 本身是不提供 <Svg /> <Circle /> 这些UI组件的,这里需要引入 react-native-svg 来提供相关 UI 模块。

import { Svg, Circle } from 'react-native-svg'

但此时的 <Circle /> 并不具备动画能力, 这里需要用到 createAnimatedComponent() 让组件变成 动画组件

import { Circle } from 'react-native-svg'
import Animated from 'react-native-reanimated'
// 让 Circle 转变成为 动画组件
const AnimatedCircle = Animated.createAnimatedComponent(Circle)

STEP2 – 初始化动画帧 shareValue

常规操作, 初始化动画帧

import { useShareValue } from 'react-native-reanimated'
const Spin = () => {
  // 初始化动画帧,并且设置初始值为 0
  const numAni = useSharedValue(0)
  return (
    //...
  )
}

STEP3 – 单位处理

根据 web 端的 animation, 我们需要对 两个属性配置 动画:

  • 父级 <View />style 样式中的 transform 属性里面的 rotate
  • <Circle /> 里面的 strokeDashoffset 属性

rotate

rotate 的数值变换是线性的

  • 0% - 100% 变化过程是 0 - 360

strokeDashoffset

strokeDashoffset 的数值变换并不是线性的:

  • 0% - 50% – 变化过程是 187 - 46.75
  • 50% - 100% – 变化过程是 46.75 - 187

假设我们让 动画帧 numAni0 - 1 开始过度, 为了对齐 strokeDashoffset, rotate 的单位, 我们需要借助 interpolate() 方法来实现

rotate 的数值变换转换, 可以这样写:

const rotate = interpolate(numAni.value, [0, 1], [0, 360])

strokeDashoffset实现起来也不复杂:

const strokeDashoffset = interpolate(numAni.value, [0, 0.5, 1], [187, 46.75, 187])

STEP4 – 描述动画过程

Circle 里面的动画是需要通过修改 props 属性里面的 strokeDashoffset 来实现的,这里就用到 useAnimatedProps() hooks

import {
  useSharedValue,
  useAnimatedProps,
  interpolate
} from 'react-native-reanimated'
import { CircleProps } from 'react-native-svg'

// step 1: 初始化 动画组件 circle
const AnimatedCircle = Animated.createAnimatedComponent(Circle)

const Spin = () => {
  // step 2: 初始化动画帧
  const numAni = useSharedValue(0)
  
  // step 3.2 初始化 circle 动画属性变化
  const animatedCircleProps = useAnimatedProps<CircleProps>(() => {
    // 单位转换
    const strokeDashoffset = interpolate(numAni.value, [0, 0.5, 1], [187, 46.75, 187])
    return {
      strokeDashoffset
    }
  })
  // step 3: 描述初始化动画
  useEffect(() => {
      //...
  }, [numAni])
  // step 4: 渲染
  return (
      //...
  )
}

父级 View 是通过 改变样式实现动画, 这里用 useAnimatedStyle 实现

import {
  useAnimatedStyle,
  useSharedValue,
  interpolate
} from 'react-native-reanimated'

const Spin = () => {
  // step 2: 初始化动画帧
  const numAni = useSharedValue(0)
  
  // step 3.1 初始化 view 动画样式变化
  const animatedViewStyle = useAnimatedStyle(() => {
    // 单位转换
    const rotate = interpolate(numAni.value, [0, 1], [0, 360])
    return {
      transform: [{ rotate: `${rotate}deg` }]
    }
  })
  return (
    //...
  )
}

然后是描述动画

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

const Spin = () => {
  // step 2: 初始化动画帧
  const numAni = useSharedValue(0)
  
  // step 3.1: 初始化 view 动画样式变化
  const animatedViewStyle = useAnimatedStyle(() => {
    //...
  })
  
  // step 3.2: 初始化 circle 动画属性
  const animatedCircleProps = useAnimatedProps<CircleProps>(() => {
    //...
  })
  
  // step 4: 描述动画
  useEffect(() => {
    numAni.value = withRepeat(
      withTiming(1, { duration: 1400, easing: Easing.inOut(Easing.ease) }),
      -1,
      false
    )
    return () => {
      // 回收操作
      cancelAnimation(numAni)
      numAni.value = 0
    }
  }, [numAni])
  
  return (
    //...
  )
  
}

STEP5 – 渲染

最后就是绑定动画 UI 组件了

通过 Animated.createAnimatedComponent() 创建的 AnimatedCircle 新增了一个 animatedProps 属性,让我们绑定动画

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

// step 1: 初始化 动画组件 circle
const AnimatedCircle = Animated.createAnimatedComponent(Circle)

const Spin = () => {
  // step 2: 初始化动画帧
  const numAni = useSharedValue(0)
  
  // step 3.1: 初始化 view 动画样式变化
  const animatedViewStyle = useAnimatedStyle(() => {
    //...
  })
  
  // step 3.2: 初始化 circle 动画属性
  const animatedCircleProps = useAnimatedProps<CircleProps>(() => {
    //...
  })
  
  // step 4: 描述动画
  useEffect(() => {
    //...
  }, [numAni])
  
  // step 5: 渲染
  return (
    <Animated.View style={[styles.spin__icon, animatedViewStyle]}>
      <Animated.View style={[styles.spin__icon__main, animatedCircleStyle]}>
        <Svg width='100%' height='100%' viewBox='0 0 60 60'>
          <AnimatedCircle
            animatedProps={animatedCircleProps}
            fill='none'
            strokeWidth={6}
            strokeLinecap='round'
            cx={30}
            cy={30}
            r={27}
            strokeDasharray={187}
            strokeDashoffset={46.75}
            stroke='#fac200'
          />
        </Svg>
      </Animated.View>
    </Animated.View>
  )
}

完整代码展示

import { View, StyleSheet } from 'react-native';
import { useEffect } from 'react'
import { Circle, Svg, CircleProps } from 'react-native-svg'
import Animated, {
useAnimatedStyle,
withRepeat,
withTiming,
useSharedValue,
cancelAnimation,
Easing,
useAnimatedProps,
interpolate
} from 'react-native-reanimated'
// step 1: 初始化 动画组件 circle
const AnimatedCircle = Animated.createAnimatedComponent(Circle)
const Spin: () => JSX.Element = () => {
// step 2: 初始化动画帧
const numAni = useSharedValue(0)
// step 3.1 初始化 view 动画样式变化
const animatedViewStyle = useAnimatedStyle(() => {
const rotate = interpolate(numAni.value, [0, 1], [0, 360])
return {
transform: [{ rotate: `${rotate}deg` }]
}
})
// step 3.2 初始化 circle 动画属性变化
const animatedCircleProps = useAnimatedProps<CircleProps>(() => {
const strokeDashoffset = interpolate(numAni.value, [0, 0.5, 1], [187, 46.75, 187])
return {
strokeDashoffset
}
})
// step 3.3 初始化 circle 动画样式变化
const animatedCircleStyle = useAnimatedStyle(() => {
const rotate = interpolate(numAni.value, [0, 0.5, 1], [0, 90, 360])
return {
transform: [{ rotate: `${rotate}deg` }]
}
})
// step 4 描述动画
useEffect(() => {
numAni.value = withRepeat(
withTiming(1, { duration: 1400, easing: Easing.inOut(Easing.ease) }),
-1,
false
)
return () => {
cancelAnimation(numAni)
numAni.value = 0
}
}, [numAni])
// step 4 渲染
return (
<Animated.View style={[styles.spin__icon, animatedViewStyle]}>
<Animated.View style={[styles.spin__icon__main, animatedCircleStyle]}>
<Svg width='100%' height='100%' viewBox='0 0 60 60'>
<AnimatedCircle
animatedProps={animatedCircleProps}
fill='none'
strokeWidth={6}
strokeLinecap='round'
cx={30}
cy={30}
r={27}
strokeDasharray={187}
strokeDashoffset={46.75}
stroke='#fac200'
/>
</Svg>
</Animated.View>
</Animated.View>
)
}
export default function App() {
return <View style={styles.body}><Spin /></View>
}
const styles = StyleSheet.create({
body: {
display: 'flex',
minHeight: '100%',
alignItems: 'center',
justifyContent: 'center'
},
spin__icon: {
display: 'flex',
width: 120,
height: 120
},
spin__icon__main: {
display: 'flex',
width: 120,
height: 120
}
});

效果展示: 这里

这里需要在手机上观看, web 端 动画是不转的

最后

从上述例子可以看出,在 RN 上面 实现复杂动画的时间成本 要比 web 上要多上不少。对于时间把握上要预留更多的时间给自己,别含泪加班效率低了。

参考资料

原文链接:https://juejin.cn/post/7255581952873676837 作者:jackness

(0)
上一篇 2023年6月29日 上午10:42
下一篇 2023年7月15日 上午10:05

相关推荐

发表回复

登录后才能评论