React Native的Animated动画原理浅谈

在React Native中创建一个动画有多种方式。

  1. 使用react-native包的Animated组件创建动画
  2. 使用react-native包的LayoutAnimation创建布局动画
  3. 使用Web API标准的requestAnimationFrame控制动画

以上是目前常用的几种在React Native中创建动画的方式,Animated动画可以进行nativeDriver加速,但是如果属性不支持,将会使用requestAnimationFrame实现计算。LayoutAnimation控制粒度不及Animated细。

本文主要来说说Animated.Value及Animated.ValueXY

使用Animated创建一个动画

先通过使用Animated创建一个动画回顾一下React Native中Animated的使用。

class AnimatedPlayground extends Component {
  state = {
    size: new Animated.Value(50) // 动画属性初始值
  }
  componentDidMount () {
    Animated.timing(this.state.size, {
      toValue: 200 // 下一阶段值
    }).start() // 开始动画
  }
  render () {
    return (
      <Animated.View style={{
        backgroundColor: '#000',
        width: this.state.size, // 将动画值赋给元素
        height: this.state.size
      }}/>
    )
  }
}

使用Animated做动画需要注意几个地方

  1. 元素需要变化的属性值必须用Animated.Value或者Animated.ValueXY初始化。
  2. 需要进行动画的元素需要转换成Animated的组件,Animated提供了Animated.ViewAnimated.TextAnimated.ImageAnimated.ScrollView ,其他组件通过Animated.createAnimatedComponent()方法处理后即可使用Animated.Value值作为属性
  3. 调用Animated的方法创建一个动画对象常用的方法有Animated.decayAnimated.timingAnimated.spring
  4. 调用第三步生成对象的start()方法即可触发动画

Animated.timing()方法

在Animated创建动画的方法中,timing是最简单易懂的一个,这里看一下timing方法内部都做了什么。

注意:文中的源码基于0.55.4版本的react-native源码

// react-native/Libraries/Animated/src/AnimatedImplementation.js#181

const timing = function(
  value: AnimatedValue | AnimatedValueXY,
  config: TimingAnimationConfig,
): CompositeAnimation {
  const start = function(
    animatedValue: AnimatedValue | AnimatedValueXY,
    configuration: TimingAnimationConfig,
    callback?: ?EndCallback,
  ): void {
    callback = _combineCallbacks(callback, configuration);
    const singleValue: any = animatedValue;
    const singleConfig: any = configuration;
    singleValue.stopTracking();
    if (configuration.toValue instanceof AnimatedNode) {
      singleValue.track(
        new AnimatedTracking(
          singleValue,
          configuration.toValue,
          TimingAnimation,
          singleConfig,
          callback,
        ),
      );
    } else {
      singleValue.animate(new TimingAnimation(singleConfig), callback);
    }
  };

  return (
    maybeVectorAnim(value, config, timing) || {
      start: function(callback?: ?EndCallback): void {
        start(value, config, callback);
      },

      stop: function(): void {
        value.stopAnimation();
      },

      reset: function(): void {
        value.resetAnimation();
      },

      _startNativeLoop: function(iterations?: number): void {
        const singleConfig = {...config, iterations};
        start(value, singleConfig);
      },

      _isUsingNativeDriver: function(): boolean {
        return config.useNativeDriver || false;
      },
    }
  );
};

timing方法会返回一个对象,在返回前会尝试使用maybeVectorAnim进行坐标值的准换,如果值不是坐标值,将会返回一个对象。在上面的例子中,对timing返回的对象调用start()方法会开始动画,所以start方法是触发动画的关键。跟随代码可以找到最后会调用Animated.Value的animate方法。

// react-native/Libraries/Animated/src/nodes/AnimatedValue.js#278

animate(animation: Animation, callback: ?EndCallback): void {
  let handle = null;
  if (animation.__isInteraction) {
    handle = InteractionManager.createInteractionHandle();
  }
  const previousAnimation = this._animation;
  this._animation && this._animation.stop();
  this._animation = animation;
  animation.start(
    this._value,
    value => {
      // Natively driven animations will never call into that callback, therefore we can always
      // pass flush = true to allow the updated value to propagate to native with setNativeProps
      this._updateValue(value, true /* flush */);
    },
    result => {
      this._animation = null;
      if (handle !== null) {
        InteractionManager.clearInteractionHandle(handle);
      }
      callback && callback(result);
    },
    previousAnimation,
    this,
  );
}

这个方法通过调用传入的animation.start方法处理值更改,当值update时调用自身的_updateValue最后会调用到_flush,这个方法是一个关键方法,阅读以下它的实现

// react-native/Libraries/Animated/src/nodes/AnimatedValue.js#51

function _flush(rootNode: AnimatedValue): void {
  const animatedStyles = new Set();
  function findAnimatedStyles(node) {
    /* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
     * error found when Flow v0.68 was deployed. To see the error delete this
     * comment and run Flow. */
    if (typeof node.update === 'function') {
      animatedStyles.add(node);
    } else {
      node.__getChildren().forEach(findAnimatedStyles);
    }
  }
  findAnimatedStyles(rootNode);
  /* $FlowFixMe */
  animatedStyles.forEach(animatedStyle => animatedStyle.update());
}

它会遍历所有子节点,找出带有update方法的节点,并调用他们的update方法。好了,Animated.Value的代码阅读展示到这里,接下来看看createAnimatedComponent这个方法主要做了什么。

createAnimatedComponent 高阶组件

打开它的源码可以看到它是一个高阶组件,它返回的组件中有一个_propsAnimated的变量,在UNSAFE_componentWillMount中调用_attachProps进行初始化,传入了props和一个函数_animatedPropsCallback,这个传入函数的作用是通过setNativeProps更新最终的Component。来看一下AnimatedProps的实现

// react-native/Libraries/Animated/src/nodes/AnimatedProps.js

class AnimatedProps extends AnimatedNode {
	...
	__attach(): void {
    for (const key in this._props) {
      const value = this._props[key];
      if (value instanceof AnimatedNode) {
        value.__addChild(this);
      }
    }
  }
  ...
  update(): void {
    this._callback();
  }
}

里面的update方法将会调用传入的回调,回到上面说的_flush方法,可以大概知道了Animated的原理了。但是这里还有一个问题就是什么时候将Value和组件关联起来的,就是为什么_flush能找到AnimatedProps的update方法。 上面代码中我贴出了__attach方法,改方法会在初始化的时候调用,最终会把this传入value.__addChild,这里的value就是通过props传入的Animated.Value。在_flush中会调用node.__getChildren().forEach(findAnimatedStyles);,它将会返回在前面添加的Child。

到这里就能理解在React Native的Animated动画实现流程了。

文章只是简单分析了一下部分源码,并没有完全分析,源码中还有很多优化及功能代码,比如detach、事件通知等。