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.Value
是AnimatedValue
类的实例,源码在:nodes/AnimatedValue.js。
AnimatedValue
有几个特点:
维护自身值的变化
有setValue
,__getValue
,_updateValue
等方法
管理自身调用的其他实例对象
class AnimatedValue extends AnimatedWithChildren (72)
AnimatedValue
继承自 AnimatedWithChildren
,使的AnimatedValue
实例拥有_children
数组属性,与相关操作_children
的add
或remove
方法。(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)
AnimatedValue
的animate
方法中,其实是调用了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._propsAnimated
是AniamtedProps
类的实例,AnimatedComponent
组件的props
由AniamtedProps
维护,关于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.View
即AnimatedComponent
,在初次渲染时,构建了如下所属关系:
AnimatedComponent
<= AnimatedProps
<= AnimatedStyle
(<= AniamtedTransform
)<= AnimatedValue
其中,AnimatedValue.__children
保留了AnimatedStyle
实例,AnimatedStyle.__children
保留了AnimatedProps
实例,AnimatedProps
保留了AnimatedComponent
的forceUpdate
回调函数,以此满足了由下至上的遍历方式,并驱动组件更新。
完整过程
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
自底向上遍历:跨组件通信,底层组件驱动嵌套的上层组件更新。