React-Native Animated 动画源码浅析

引子

Aniamted组件是React-Native官方出品的动画组件。在本文开始之前,我们首先来看看来自官网的一个例子,看看如何使用Animated组件。

import React from 'react';
import { Animated, Text, View } from 'react-native';

class FadeInView extends React.Component {
  state = {
    fadeAnim: new Animated.Value(0),  // Initial value for opacity: 0
  }

  componentDidMount() {
    Animated.timing(                  // Animate over time
      this.state.fadeAnim,            // The animated value to drive
      {
        toValue: 1,                   // Animate to opacity: 1 (opaque)
        duration: 10000,              // Make it take a while
      }
    ).start();                        // Starts the animation
  }

  render() {
    let { fadeAnim } = this.state;

    return (
      <Animated.View                 // Special animatable View
        style={{
          ...this.props.style,
          opacity: fadeAnim,         // Bind opacity to animated value
        }}
      >
        {this.props.children}
      </Animated.View>
    );
  }
}

// You can then use your `FadeInView` in place of a `View` in your components:
export default class App extends React.Component {
  render() {
    return (
      <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
        <FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}>
          <Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text>
        </FadeInView>
      </View>
    )
  }
}

这个例子展示了如何写一个FadeInView动画组件,呈现了在10s内驱动一个组件的透明度从0到1渐变的过程。

要点

从上面这个例子中,我们可以归纳出Animated动画组件的几个要点:

  • new Animated.Value(0) => 即 Animated.Value 动画值
  • Animated.timing(value, { toValue }).start() => 即Animated.timing动画函数
  • <Animated.View style={ animatedValue }/> => 即Animated.View动画组件

问题

在下文的源码分析中,我们将带着以下几个问题来进行分析:

  • 动画组件 Animated.View style 如何更新?
  • 动画值 Animated.Value 与 动画组件 Animated.View之间有什么关系?
  • 动画函数 Animated.timing 与 动画值 Animated.Value 之间有什么关系?

源码分析

本文分析的源码在:react-native@0.49.3/Libraries/Animated/src

本文分析篇幅仅限不使用原生动画驱动的场景,原生动画驱动使用了NativeAnimatedModule,鉴于笔者没有相关原生开发经验,在此不展开。不过据笔者阅读源码所知:原生动画驱动并不会驱动组件重复渲染,而是直接修改相关属性

调用栈

首先来看看动画执行时的调用栈,之后我们再详细分析:

我们先来了解一下,作为动画过程的关键变化值Animated.Value

动画值Animated.Value

动画值的Animated.ValueAnimatedValue类的实例,源码在:nodes/AnimatedValue.js
AnimatedValue有几个特点:

维护自身值的变化

setValue,__getValue,_updateValue等方法

管理自身调用的其他实例对象

class AnimatedValue extends AnimatedWithChildren (72)

AnimatedValue 继承自 AnimatedWithChildren,使的AnimatedValue实例拥有_children数组属性,与相关操作_childrenaddremove方法。(AnimatedWithChildren之后也有相关介绍)

在自身值变化会触发其他实例对象的update方法

由上图调用栈我们知道:animate => _updateValue => _flush => animateStyle.update (63)

我们先来看下animate方法:

animate(animation: Animation, callback: ?EndCallback): void {
    ......
    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,
    );
  }

_updateValue会作为动画函数的回调函数,在数据更新时触发。而_updateValue会更新自身value,并触发flush函数。

我们再来看下flush方法:

function _flush(rootNode: AnimatedValue): void {
  const animatedStyles = new Set();
  function findAnimatedStyles(node) {
    if (typeof node.update === 'function') {
      animatedStyles.add(node);
    } else {
      node.__getChildren().forEach(findAnimatedStyles);
    }
  }
  findAnimatedStyles(rootNode);
  /* $FlowFixMe */
  animatedStyles.forEach(animatedStyle => animatedStyle.update());
}

flush 函数通过向上递归遍历找到最近的使用该 value,并且拥有update方法的实例对象,并触发该类的 update 方法。

接下来我们看看,动画怎么执行。

动画函数 Animated.timing

动画开始start其实是触发AnimatedValue类的animate方法

singleValue.animate(new TimingAnimation(singleConfig), callback);(AnimatedImplementation.js 200)

AnimatedValueanimate方法中,其实是调用了TimingAnimation类的start方法

TimingAnimation类的源码在:animations/TimingAnimation.js

我们来看下start方法:

  start(
    fromValue: number,
    onUpdate: (value: number) => void,
    onEnd: ?EndCallback,
    previousAnimation: ?Animation,
    animatedValue: AnimatedValue,
  ): void {
      ......
    const start = () => {
      // Animations that sometimes have 0 duration and sometimes do not
      // still need to use the native driver when duration is 0 so as to
      // not cause intermixed JS and native animations.
      if (this._duration === 0 && !this._useNativeDriver) {
        this._onUpdate(this._toValue);
        this.__debouncedOnEnd({finished: true});
      } else {
        this._startTime = Date.now();
        if (this._useNativeDriver) {
          this.__startNativeAnimation(animatedValue);
        } else {
          this._animationFrame = requestAnimationFrame(
            this.onUpdate.bind(this),
          );
        }
      }
    };
    if (this._delay) {
      this._timeout = setTimeout(start, this._delay);
    } else {
      start();
    }
  }

即:animation.start => animation.onUpdate (requestAnimationFrame)

onUpdate方法,则是根据当前选择的缓动方法easing计算更新时机,并在requestAnimationFrame触发回调函数(AnimatedValue_udpateValue)。

接下来,是最重要的组件环节了。

动画组件 Animated.View

Animated.View是封装过的React组件,名称是AnimatedComponent,源码在createAnimatedComponent.js

props属性获取

render() {
  const props = this._propsAnimated.__getValue();
  ......
}

this._propsAnimatedAniamtedProps类的实例,AnimatedComponent组件的propsAniamtedProps维护,关于AniamtedProps,之后会详细介绍。

组件更新

AnimatedComponent组件的生命周期方法componentWillMount/componentWillReceiveProps中都调用了_attachProps方法,我们先来看下这个方法:

_attachProps(nextProps) {
  ......
  const callback = () => {
    if (
      !AnimatedComponent.__skipSetNativeProps_FOR_TESTS_ONLY &&
      this._component.setNativeProps
    ) {
      if (!this._propsAnimated.__isNative) {
        this._component.setNativeProps(
          this._propsAnimated.__getAnimatedValue(),
        );
      } else {
        throw new Error(
          'Attempting to run JS driven animation on animated ' +
            'node that has been moved to "native" earlier by starting an ' +
            'animation with `useNativeDriver: true`',
        );
      }
    } else {
      this.forceUpdate();
    }
  };

  this._propsAnimated = new AnimatedProps(nextProps, callback);
  ......
}

即:_attachProps => new AnimatedProps(props, callback(forceUpdate)) (104)

我们注意到,组件将forceUpdate封装成了回调函数,并交给了AniamtedProps

接下来,我们来详细看看AnimatedProps

props维护者AnimatedProps

AnimatedProps的源码在nodes/AnimatedProps.js,该类的构造函数如下:

class AnimatedProps extends AnimatedNode {
  _props: Object;
  _animatedView: any;
  _callback: () => void;

  constructor(props: Object, callback: () => void) {
    super();
    if (props.style) {
      props = {
        ...props,
        style: new AnimatedStyle(props.style),
      };
    }
    this._props = props;
    this._callback = callback;
    this.__attach();
  }

注意这行:this._callback = callback;

Animated.View组件,将触发自身forceUpdate的回调函数传给AnimatedProps类的构造函数,以在适当的时机触发该回调函数。

与此相关的是AnimatedProps的实例方法update

update(): void {
    this._callback();
 }

所以:执行update方法,便会触发从动画组件Animated.View传过来的回调函数,在不使用原生动画驱动的情况下,触发forceUpdate,使组件重新渲染。

style是一个AnimatedStyle类实例对象。AnimatedStyle类下文会详细介绍。

同样的,__attach方法很关键,我们来看一下:

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

对于使用了继承自AnimatedNode的类实例,该实例会将当前AnimatedProps实例对象,添加到_children数组中寄存,便于之后使用。

在本例中,AnimatedStyle实例对象会将AnimatedProps实例对象添加到_children数组属性中存储。

对于props值的获取,是AnimatedProps类的_getValue方法,该方法如下:

__getValue(): Object {
    const props = {};
    for (const key in this._props) {
      const value = this._props[key];
      if (value instanceof AnimatedNode) {
        if (!value.__isNative || value instanceof AnimatedStyle) {
          // We cannot use value of natively driven nodes this way as the value we have access from
          // JS may not be up to date.
          props[key] = value.__getValue();
        }
      } else if (value instanceof AnimatedEvent) {
        props[key] = value.__getHandler();
      } else {
        props[key] = value;
      }
    }
    return props;
  }

从这里我们知道,对于style属性,AnimatedProps直接取自AnimatedStyle_getValue

接下来,我们来看下AnimatedStyle

style的维护者AnimatedStyle

在例子中以及经过上面的代码分析,我们得知:AnimatedValue的实例对象交给了AnimatedStyle

我们来看看AnimatedStyle的构造函数。

class AnimatedStyle extends AnimatedWithChildren {
    ......
    constructor(style: any) {
    ......
    if (style.transform) {
      style = {
        ...style,
        transform: new AnimatedTransform(style.transform),
      };
    }
    this._style = style;
    }
}

在此说明一下,如果style 中有transform属性,则会创建一个AnimatedTransform类实例来管理transform

我们知道,AnimatedValue也是style的属性,我们来关注下与AnimatedValue相关的有两个主要地方:

  • __attach 方法

该方法保证将使用了AnimatedValue的当前实例被添加到_children数组中。

__attach(): void {
    for (const key in this._style) {
      const value = this._style[key];
      if (value instanceof AnimatedNode) {
        value.__addChild(this);
      }
    }
  }

不像AnimatedProps,在AnimatedStyle类的构造函数中,并没有调用__attach方法。通过观察函数调用栈,我们发现了端倪:

AnimatedWithChildren__addChild方法,会在_children数组为空时,调用__attach方法。因为AnimatedStyle继承自AnimateWithChildren也没有重写__addChild,所以会执行同样的逻辑。

 __addChild(child: AnimatedNode): void {
    if (this._children.length === 0) {
      this.__attach();
    }
    ......
  }

(nodes/AnimatedWithChildren.js)

所以:AnimatedProps__attach方法里,AnimatedStyle 执行__addChild时,便会自动执行__attach完成下层调用关系 (AnimatedValue) 的添加

  • AnimateValue的获取
__getValue(): Object {
    return this._walkStyleAndGetValues(this._style);
  }

_walkStyleAndGetValues(style) {
    const updatedStyle = {};
    for (const key in style) {
      const value = style[key];
      if (value instanceof AnimatedNode) {
        if (!value.__isNative) {
          // We cannot use value of natively driven nodes this way as the value we have access from
          // JS may not be up to date.
          updatedStyle[key] = value.__getValue();
        }
      } else if (value && !Array.isArray(value) && typeof value === 'object') {
        // Support animating nested values (for example: shadowOffset.height)
        updatedStyle[key] = this._walkStyleAndGetValues(value);
      } else {
        updatedStyle[key] = value;
      }
    }
    return updatedStyle;
  }

这里保证了AnimatedStyle能够拿到AnimatedValue的值。

归纳

由此可知,动画组件Animated.ViewAnimatedComponent,在初次渲染时,构建了如下所属关系:

AnimatedComponent <= AnimatedProps <= AnimatedStyle (<= AniamtedTransform)<= AnimatedValue

其中,AnimatedValue.__children保留了AnimatedStyle实例,AnimatedStyle.__children保留了AnimatedProps实例,AnimatedProps保留了AnimatedComponentforceUpdate回调函数,以此满足了由下至上的遍历方式,并驱动组件更新。

完整过程

flow

Animated.View组件在初始化渲染的同时,使得Animated.Value保存了调用自身的类实例对象。

timing.start 动画开始执行,Animated.Value会以一定时间规律触发自身值改变,并在改变的同时,触发flush函数,通过向上遍历调用实例的update方法。

在整个调用子集中,只有AnimatedProps类有 update 方法,而AnimatedProps类的update函数,只是执行来自Aimated.View组件的回调函数。

动画组件Animated.View的回调函数里调用了forceUpdate,该函数执行,组件强行更新,并重新render获得新的style

结论

不使用原生动画驱动的场景下,Animated.View会在动画函数根据缓动方式的更新时机触发在requestAnimationFrame 包裹下的forceUpdate ,使组件持续更新并拿到最新的style

题外

在本文中,我们可以学到几个套路:

React 动画解决方案:缓动函数 + requestAnimationFrame + forceUpdate

自底向上遍历:跨组件通信,底层组件驱动嵌套的上层组件更新。

标签: none

添加新评论