众所周知, RN
在做动画时并没有像 web
那般 通过 样式中提供的 animation
, keyframe
, transiton
即可满足一般需求。今天我们聊聊一个有些复杂的 加载动画
的实现过程。
阅读本文章,你可能收获以下知识点:
Animated.createAnimatedComponent()
创建动画组件interpolate()
插值函数useAnimateProps()
useAnimatedStyle()
效果预览
这是一个 yy
这边通用的加载动画
今天我们就聊聊中间的这个动画
可以看到 这个加载动画并不是单纯的旋转,而是补充了一些动画效果的, 看了下 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
假设我们让 动画帧 numAni
从 0 - 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