5分钟实现一个React-Native数字跳动动画

2,347 阅读3分钟

在React-Native中通过Animated可以去实现一些贴近原生的动画效果体验,今天就来花5分中时间结合Animated采用仿动画的形式实现一个数字跳动的动画。

首先明确一下需求

我们需要将字符串: 倒计时11:0 -> 倒计时10:55的过程用RN动画形式展示

demo

实现思路

这时候我们可以明确实现的思路就是

先将数据源进行拆分 -> 将数据源中整数部分与字符串进行拆分 -> 拆分部分字符串单独渲染,数字部分进行动画实现

数据源拆分

首先需要针对数据源拆分成整数及非整数部分用来进行分别渲染

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,在使用场景较多的情况下不推荐使用。