在React-Native中通过Animated可以去实现一些贴近原生的动画效果体验,今天就来花5分中时间结合Animated采用仿动画的形式实现一个数字跳动的动画。
首先明确一下需求
我们需要将字符串: 倒计时11:0 -> 倒计时10:55的过程用RN动画形式展示
实现思路
这时候我们可以明确实现的思路就是
先将数据源进行拆分 -> 将数据源中整数部分与字符串进行拆分 -> 拆分部分字符串单独渲染,数字部分进行动画实现
数据源拆分
首先需要针对数据源拆分成整数及非整数部分用来进行分别渲染
const splitStringByNumber = (str = '') => {
const arr = [];
if (typeof str !== 'string') return arr;
const trimStr = str.trim();
if (trimStr.length === 0) return arr;
const strArr = trimStr.split('');
const len = strArr.length;
let tag = '';
let number = '';
strArr.forEach((it, _idx) => {
if (Number.isInteger(+it)) {
tag !== '' && arr.push(tag);
number += it;
!Number.isInteger(+strArr[_idx + 1]) && (arr.push(+number), (number = ''));
tag = '';
} else {
tag += it;
_idx === len - 1 && arr.push(tag);
}
});
return arr;
};
通过该方法可以将数据进行拆分
"倒计时11:0" -> ["倒计时", 11, ":", 0]
看到这里大家可以知道接下来剧情如何发展了吧,剩下的就是将数字及字符串进行单独渲染
数字动画组件实现
首先先进行数字端的组件实现,要如何做到数值状态从初始值到目标值例如从0-100的一个转化呢,通常我们可能会通过定时器setInterval的方式去做状态更新,但这种方案在线性动画中容易实现,但一旦涉及到例如Animated的bezier效果或其他一些复杂的数值变动动画就会显得捉襟见肘,在实现方面也会比较复杂。
那如何来做一个贴合动画效果的跳动实现呢,这时候就可以考虑使用Animated原生动画并通过事件监听的方式去获取对应帧的动画插值,然后通过进行state的改变需要展现的数值交给react进行渲染。
const magicValue = new Animated.Value(props.initialValue);
this.state = {
magicValue,
displayValue: props.text,
};
magicValue.addListener(({ value }) => {
// 由于value不为整数,需进行一步取整
this.setState({ displayValue: ~~value });
});
接下来看看数字动画部分的整体代码,可以通过暴露动画相关执行参数去支持开发自定义相关动画模式
import React from 'react';
import { Animated, Easing } from 'react-native';
import PropTypes from 'prop-types';
export default class AnimatedNumber extends React.PureComponent {
static propTypes = {
textStyle: Text.propTypes.style,
duration: PropTypes.number,
animateType: PropTypes.oneOf(['timing', 'spring']),
animateConfig: PropTypes.object,
textConfig: PropTypes.object,
text: PropTypes.number,
initialValue: PropTypes.number,
};
static defaultProps = {
textStyle: {},
duration: 300,
animateType: 'timing',
animateConfig: {},
textConfig: {},
text: 0,
initialValue: 0,
};
constructor(props) {
super(props);
const magicValue = new Animated.Value(props.initialValue);
this.state = {
magicValue,
displayValue: props.text,
};
magicValue.addListener(({ value }) => {
this.setState({ displayValue: ~~value });
});
}
componentDidMount() {
requestAnimationFrame(() => {
this.animateStart();
});
}
componentWillReceiveProps(nextProps) {
if (this.props.text !== nextProps.text) {
this.animateStart(nextProps);
}
}
animateStart = props => {
const { text, animateType = 'timing', animateConfig } = props || this.props;
Animated[animateType](this.state.magicValue, {
toValue: text,
easing: Easing.ease,
...animateConfig,
}).start();
};
render() {
const { textConfig, textStyle } = this.props;
const { displayValue } = this.state;
return (
<Animated.Text style={textStyle} {...textConfig}>
{displayValue}
</Animated.Text>
);
}
}
最终源码
接下来我们就可以将数据源拆解方法与数字动画进行一个结合
import React from 'react';
import { View, StyleSheet, ViewPropTypes, Text } from 'react-native';
import AnimatedNumber from './animate-number';
import splitStringByNumber from './splitTextByNumber';
const MagicText = ({ text, duration, style, textStyle, animateConfig, ...rest }) => (
<View style={[styles.animateContainer, style]}>
{splitStringByNumber(text).map((it, _idx) => {
const isString = typeof it === 'string';
const Item = isString ? Text : AnimatedNumber;
const key = `${it}${_idx}`;
return (
<Item
key={key}
text={it}
style={styles.contentText}
textStyle={textStyle}
initialValue={0}
animateConfig={{
duration,
useNativeDriver: true,
...animateConfig
}}
{...rest}
/>
);
})}
</View>
);
<MagicText text="倒计时11:0" />
--> nextState
<MagicText text="倒计时10:55" />
结尾
RN中的动画效果可以贴近原生,采用原生提供的Animated已经可以实现很多复杂场景的动画,但离原生效果还是有些差距,通过监听动画获取插值的方式可以偷懒靠Animated实现各种动画类型下的数字跳动,但会这种方式的动画实现没有直接采用Animated来的顺畅,会触发毫秒级的render,在使用场景较多的情况下不推荐使用。