mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-17 03:05:04 +08:00
Execute Animated.js declarative animation on UIThread on Android.
Summary:This is the first from the series of PRs I'm going to be sending shorty that would let Animated.js animations to run off the JS thread (for Android only). This PR introduce a new native module that will be used for offloading animations - NativeAnimatedModule. It has a simple API that allows for animated nodes management via methods like: create/drop animated node, connect/disconnect nodes, start animation of a value node, attach/detach animated from a native view. Similarly to how we handle UIManager view hierarchy updates we create a queue of animated graph operations that are then executed on the UI thread. This isolates us from problems that may be caused by concurrent updates of animated graph while UI thread is "executing" the animation. The most important class NativeAnimatedNodesManager.java implements a management interface for animated nodes graph as well as implements a graph traversal algorithm that is run for each animation frame. For each animation frame we visit animated nodes th Closes https://github.com/facebook/react-native/pull/6466 Differential Revision: D3092739 Pulled By: astreet fb-gh-sync-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba shipit-source-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba
This commit is contained in:
committed by
Facebook Github Bot 5
parent
bd8007300f
commit
65ccdffc8d
@@ -18,7 +18,9 @@ var React = require('React');
|
||||
var Set = require('Set');
|
||||
var SpringConfig = require('SpringConfig');
|
||||
var ViewStylePropTypes = require('ViewStylePropTypes');
|
||||
var NativeAnimatedHelper = require('NativeAnimatedHelper');
|
||||
|
||||
var findNodeHandle = require('findNodeHandle');
|
||||
var flattenStyle = require('flattenStyle');
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
var requestAnimationFrame = require('fbjs/lib/requestAnimationFrame');
|
||||
@@ -28,20 +30,50 @@ import type { InterpolationConfigType } from 'Interpolation';
|
||||
type EndResult = {finished: bool};
|
||||
type EndCallback = (result: EndResult) => void;
|
||||
|
||||
var NativeAnimatedAPI = NativeAnimatedHelper.API;
|
||||
|
||||
// Note(vjeux): this would be better as an interface but flow doesn't
|
||||
// support them yet
|
||||
class Animated {
|
||||
__attach(): void {}
|
||||
__detach(): void {}
|
||||
__detach(): void {
|
||||
if (this.__isNative && this.__nativeTag != null) {
|
||||
NativeAnimatedAPI.dropAnimatedNode(this.__nativeTag);
|
||||
this.__nativeTag = undefined;
|
||||
}
|
||||
}
|
||||
__getValue(): any {}
|
||||
__getAnimatedValue(): any { return this.__getValue(); }
|
||||
__addChild(child: Animated) {}
|
||||
__removeChild(child: Animated) {}
|
||||
__getChildren(): Array<Animated> { return []; }
|
||||
|
||||
/* Methods and props used by native Animated impl */
|
||||
__isNative: bool;
|
||||
__nativeTag: ?number;
|
||||
__makeNative() {
|
||||
if (!this.__isNative) {
|
||||
throw new Error('This node cannot be made a "native" animated node');
|
||||
}
|
||||
}
|
||||
__getNativeTag(): number {
|
||||
NativeAnimatedHelper.assertNativeAnimatedModule();
|
||||
invariant(this.__isNative, 'Attempt to get native tag from node not marked as "native"');
|
||||
if (this.__nativeTag == null) {
|
||||
var nativeTag: number = NativeAnimatedHelper.generateNewNodeTag();
|
||||
NativeAnimatedAPI.createAnimatedNode(nativeTag, this.__getNativeConfig());
|
||||
this.__nativeTag = nativeTag;
|
||||
}
|
||||
return this.__nativeTag;
|
||||
}
|
||||
__getNativeConfig(): Object {
|
||||
throw new Error('This JS animated node type cannot be used as native animated node');
|
||||
}
|
||||
}
|
||||
|
||||
type AnimationConfig = {
|
||||
isInteraction?: bool;
|
||||
useNativeDriver?: bool;
|
||||
};
|
||||
|
||||
// Important note: start() and stop() will only be called at most once.
|
||||
@@ -50,20 +82,37 @@ type AnimationConfig = {
|
||||
class Animation {
|
||||
__active: bool;
|
||||
__isInteraction: bool;
|
||||
__nativeTag: number;
|
||||
__onEnd: ?EndCallback;
|
||||
start(
|
||||
fromValue: number,
|
||||
onUpdate: (value: number) => void,
|
||||
onEnd: ?EndCallback,
|
||||
previousAnimation: ?Animation,
|
||||
animatedValue: AnimatedValue
|
||||
): void {}
|
||||
stop(): void {}
|
||||
_getNativeAnimationConfig(): any {
|
||||
// Subclasses that have corresponding animation implementation done in native
|
||||
// should override this method
|
||||
throw new Error('This animation type cannot be offloaded to native');
|
||||
}
|
||||
// Helper function for subclasses to make sure onEnd is only called once.
|
||||
__debouncedOnEnd(result: EndResult) {
|
||||
__debouncedOnEnd(result: EndResult): void {
|
||||
var onEnd = this.__onEnd;
|
||||
this.__onEnd = null;
|
||||
onEnd && onEnd(result);
|
||||
}
|
||||
__startNativeAnimation(animatedValue: AnimatedValue): void {
|
||||
animatedValue.__makeNative();
|
||||
this.__nativeTag = NativeAnimatedHelper.generateNewAnimationTag();
|
||||
NativeAnimatedAPI.startAnimatingNode(
|
||||
this.__nativeTag,
|
||||
animatedValue.__getNativeTag(),
|
||||
this._getNativeAnimationConfig(),
|
||||
this.__debouncedOnEnd.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedWithChildren extends Animated {
|
||||
@@ -74,11 +123,26 @@ class AnimatedWithChildren extends Animated {
|
||||
this._children = [];
|
||||
}
|
||||
|
||||
__makeNative() {
|
||||
if (!this.__isNative) {
|
||||
this.__isNative = true;
|
||||
for (var child of this._children) {
|
||||
child.__makeNative();
|
||||
NativeAnimatedAPI.connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__addChild(child: Animated): void {
|
||||
if (this._children.length === 0) {
|
||||
this.__attach();
|
||||
}
|
||||
this._children.push(child);
|
||||
if (this.__isNative) {
|
||||
// Only accept "native" animated nodes as children
|
||||
child.__makeNative();
|
||||
NativeAnimatedAPI.connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
|
||||
}
|
||||
}
|
||||
|
||||
__removeChild(child: Animated): void {
|
||||
@@ -87,6 +151,9 @@ class AnimatedWithChildren extends Animated {
|
||||
console.warn('Trying to remove a child that doesn\'t exist');
|
||||
return;
|
||||
}
|
||||
if (this.__isNative && child.__isNative) {
|
||||
NativeAnimatedAPI.disconnectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
|
||||
}
|
||||
this._children.splice(index, 1);
|
||||
if (this._children.length === 0) {
|
||||
this.__detach();
|
||||
@@ -160,6 +227,7 @@ class TimingAnimation extends Animation {
|
||||
_onUpdate: (value: number) => void;
|
||||
_animationFrame: any;
|
||||
_timeout: any;
|
||||
_useNativeDriver: bool;
|
||||
|
||||
constructor(
|
||||
config: TimingAnimationConfigSingle,
|
||||
@@ -170,12 +238,28 @@ class TimingAnimation extends Animation {
|
||||
this._duration = config.duration !== undefined ? config.duration : 500;
|
||||
this._delay = config.delay !== undefined ? config.delay : 0;
|
||||
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
|
||||
this._useNativeDriver = !!config.useNativeDriver;
|
||||
}
|
||||
|
||||
_getNativeAnimationConfig(): any {
|
||||
var frameDuration = 1000.0 / 60.0;
|
||||
var frames = [];
|
||||
for (var dt = 0.0; dt <= this._duration; dt += frameDuration) {
|
||||
frames.push(this._easing(dt / this._duration));
|
||||
}
|
||||
return {
|
||||
type: 'frames',
|
||||
frames,
|
||||
toValue: this._toValue,
|
||||
};
|
||||
}
|
||||
|
||||
start(
|
||||
fromValue: number,
|
||||
onUpdate: (value: number) => void,
|
||||
onEnd: ?EndCallback,
|
||||
previousAnimation: ?Animation,
|
||||
animatedValue: AnimatedValue
|
||||
): void {
|
||||
this.__active = true;
|
||||
this._fromValue = fromValue;
|
||||
@@ -188,7 +272,11 @@ class TimingAnimation extends Animation {
|
||||
this.__debouncedOnEnd({finished: true});
|
||||
} else {
|
||||
this._startTime = Date.now();
|
||||
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
|
||||
if (this._useNativeDriver) {
|
||||
this.__startNativeAnimation(animatedValue);
|
||||
} else {
|
||||
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
|
||||
}
|
||||
}
|
||||
};
|
||||
if (this._delay) {
|
||||
@@ -525,6 +613,7 @@ var _uniqueId = 1;
|
||||
*/
|
||||
class AnimatedValue extends AnimatedWithChildren {
|
||||
_value: number;
|
||||
_startingValue: number;
|
||||
_offset: number;
|
||||
_animation: ?Animation;
|
||||
_tracking: ?Animated;
|
||||
@@ -532,7 +621,7 @@ class AnimatedValue extends AnimatedWithChildren {
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this._value = value;
|
||||
this._startingValue = this._value = value;
|
||||
this._offset = 0;
|
||||
this._animation = null;
|
||||
this._listeners = {};
|
||||
@@ -540,6 +629,7 @@ class AnimatedValue extends AnimatedWithChildren {
|
||||
|
||||
__detach() {
|
||||
this.stopAnimation();
|
||||
super.__detach();
|
||||
}
|
||||
|
||||
__getValue(): number {
|
||||
@@ -556,6 +646,9 @@ class AnimatedValue extends AnimatedWithChildren {
|
||||
this._animation = null;
|
||||
}
|
||||
this._updateValue(value);
|
||||
if (this.__isNative) {
|
||||
NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -640,6 +733,7 @@ class AnimatedValue extends AnimatedWithChildren {
|
||||
callback && callback(result);
|
||||
},
|
||||
previousAnimation,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
@@ -666,6 +760,13 @@ class AnimatedValue extends AnimatedWithChildren {
|
||||
this._listeners[key]({value: this.__getValue()});
|
||||
}
|
||||
}
|
||||
|
||||
__getNativeConfig(): Object {
|
||||
return {
|
||||
type: 'value',
|
||||
value: this._startingValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ValueXYListenerCallback = (value: {x: number; y: number}) => void;
|
||||
@@ -1008,7 +1109,11 @@ class AnimatedStyle extends AnimatedWithChildren {
|
||||
for (var key in this._style) {
|
||||
var value = this._style[key];
|
||||
if (value instanceof Animated) {
|
||||
style[key] = value.__getValue();
|
||||
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
|
||||
style[key] = value.__getValue();
|
||||
}
|
||||
} else {
|
||||
style[key] = value;
|
||||
}
|
||||
@@ -1044,10 +1149,37 @@ class AnimatedStyle extends AnimatedWithChildren {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__makeNative() {
|
||||
super.__makeNative();
|
||||
for (var key in this._style) {
|
||||
var value = this._style[key];
|
||||
if (value instanceof Animated) {
|
||||
value.__makeNative();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__getNativeConfig(): Object {
|
||||
var styleConfig = {};
|
||||
for (let styleKey in this._style) {
|
||||
if (this._style[styleKey] instanceof Animated) {
|
||||
styleConfig[styleKey] = this._style[styleKey].__getNativeTag();
|
||||
}
|
||||
// Non-animated styles are set using `setNativeProps`, no need
|
||||
// to pass those as a part of the node config
|
||||
}
|
||||
NativeAnimatedHelper.validateStyles(styleConfig);
|
||||
return {
|
||||
type: 'style',
|
||||
style: styleConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedProps extends Animated {
|
||||
_props: Object;
|
||||
_animatedView: any;
|
||||
_callback: () => void;
|
||||
|
||||
constructor(
|
||||
@@ -1071,7 +1203,11 @@ class AnimatedProps extends Animated {
|
||||
for (var key in this._props) {
|
||||
var value = this._props[key];
|
||||
if (value instanceof Animated) {
|
||||
props[key] = value.__getValue();
|
||||
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
|
||||
props[key] = value.__getValue();
|
||||
}
|
||||
} else {
|
||||
props[key] = value;
|
||||
}
|
||||
@@ -1100,17 +1236,73 @@ class AnimatedProps extends Animated {
|
||||
}
|
||||
|
||||
__detach(): void {
|
||||
if (this.__isNative && this._animatedView) {
|
||||
this.__disconnectAnimatedView();
|
||||
}
|
||||
for (var key in this._props) {
|
||||
var value = this._props[key];
|
||||
if (value instanceof Animated) {
|
||||
value.__removeChild(this);
|
||||
}
|
||||
}
|
||||
super.__detach();
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this._callback();
|
||||
}
|
||||
|
||||
__makeNative(): void {
|
||||
if (!this.__isNative) {
|
||||
this.__isNative = true;
|
||||
for (var key in this._props) {
|
||||
var value = this._props[key];
|
||||
if (value instanceof Animated) {
|
||||
value.__makeNative();
|
||||
}
|
||||
}
|
||||
if (this._animatedView) {
|
||||
this.__connectAnimatedView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setNativeView(animatedView: any): void {
|
||||
invariant(this._animatedView === undefined, 'Animated view already set.');
|
||||
this._animatedView = animatedView;
|
||||
if (this.__isNative) {
|
||||
this.__connectAnimatedView();
|
||||
}
|
||||
}
|
||||
|
||||
__connectAnimatedView(): void {
|
||||
invariant(this.__isNative, 'Expected node to be marked as "native"');
|
||||
var nativeViewTag: ?number = findNodeHandle(this._animatedView);
|
||||
invariant(nativeViewTag != null, 'Unable to locate attached view in the native tree');
|
||||
NativeAnimatedAPI.connectAnimatedNodeToView(this.__getNativeTag(), nativeViewTag);
|
||||
}
|
||||
|
||||
__disconnectAnimatedView(): void {
|
||||
invariant(this.__isNative, 'Expected node to be marked as "native"');
|
||||
var nativeViewTag: ?number = findNodeHandle(this._animatedView);
|
||||
invariant(nativeViewTag != null, 'Unable to locate attached view in the native tree');
|
||||
NativeAnimatedAPI.disconnectAnimatedNodeFromView(this.__getNativeTag(), nativeViewTag);
|
||||
}
|
||||
|
||||
__getNativeConfig(): Object {
|
||||
var propsConfig = {};
|
||||
for (let propKey in this._props) {
|
||||
var value = this._props[propKey];
|
||||
if (value instanceof Animated) {
|
||||
propsConfig[propKey] = value.__getNativeTag();
|
||||
}
|
||||
}
|
||||
NativeAnimatedHelper.validateProps(propsConfig);
|
||||
return {
|
||||
type: 'props',
|
||||
props: propsConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createAnimatedComponent(Component: any): any {
|
||||
@@ -1131,6 +1323,10 @@ function createAnimatedComponent(Component: any): any {
|
||||
this.attachProps(this.props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._propsAnimated.setNativeView(this.refs[refName]);
|
||||
}
|
||||
|
||||
attachProps(nextProps) {
|
||||
var oldPropsAnimated = this._propsAnimated;
|
||||
|
||||
@@ -1142,8 +1338,15 @@ function createAnimatedComponent(Component: any): any {
|
||||
// forceUpdate.
|
||||
var callback = () => {
|
||||
if (this.refs[refName].setNativeProps) {
|
||||
var value = this._propsAnimated.__getAnimatedValue();
|
||||
this.refs[refName].setNativeProps(value);
|
||||
if (!this._propsAnimated.__isNative) {
|
||||
this.refs[refName].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();
|
||||
}
|
||||
@@ -1154,6 +1357,11 @@ function createAnimatedComponent(Component: any): any {
|
||||
callback,
|
||||
);
|
||||
|
||||
|
||||
if (this.refs && this.refs[refName]) {
|
||||
this._propsAnimated.setNativeView(this.refs[refName]);
|
||||
}
|
||||
|
||||
// When you call detach, it removes the element from the parent list
|
||||
// of children. If it goes to 0, then the parent also detaches itself
|
||||
// and so on.
|
||||
@@ -1232,6 +1440,7 @@ class AnimatedTracking extends Animated {
|
||||
|
||||
__detach(): void {
|
||||
this._parent.__removeChild(this);
|
||||
super.__detach();
|
||||
}
|
||||
|
||||
update(): void {
|
||||
|
||||
119
Libraries/Animated/src/NativeAnimatedHelper.js
Normal file
119
Libraries/Animated/src/NativeAnimatedHelper.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule NativeAnimatedHelper
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var NativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
|
||||
var __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */
|
||||
var __nativeAnimationTagCount = 1; /* used for started animations */
|
||||
|
||||
type EndResult = {finished: bool};
|
||||
type EndCallback = (result: EndResult) => void;
|
||||
|
||||
/**
|
||||
* Simple wrappers around NativeANimatedModule to provide flow and autocmplete support for
|
||||
* the native module methods
|
||||
*/
|
||||
var API = {
|
||||
createAnimatedNode: function(tag: number, config: Object): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.createAnimatedNode(tag, config);
|
||||
},
|
||||
connectAnimatedNodes: function(parentTag: number, childTag: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag);
|
||||
},
|
||||
disconnectAnimatedNodes: function(parentTag: number, childTag: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag);
|
||||
},
|
||||
startAnimatingNode: function(animationTag: number, nodeTag: number, config: Object, endCallback: EndCallback) {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.startAnimatingNode(nodeTag, config, endCallback);
|
||||
},
|
||||
setAnimatedNodeValue: function(nodeTag: number, value: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value);
|
||||
},
|
||||
connectAnimatedNodeToView: function(nodeTag: number, viewTag: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag);
|
||||
},
|
||||
disconnectAnimatedNodeFromView: function(nodeTag: number, viewTag: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag);
|
||||
},
|
||||
dropAnimatedNode: function(tag: number): void {
|
||||
assertNativeAnimatedModule();
|
||||
NativeAnimatedModule.dropAnimatedNode(tag);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties allowed by the native animated implementation.
|
||||
*
|
||||
* In general native animated implementation should support any numeric property that doesn't need
|
||||
* to be updated through the shadow view hierarchy (all non-layout properties). This list is limited
|
||||
* to the properties that will perform best when animated off the JS thread.
|
||||
*/
|
||||
var PROPS_WHITELIST = {
|
||||
style: {
|
||||
opacity: true,
|
||||
|
||||
/* legacy android transform properties */
|
||||
scaleX: true,
|
||||
scaleY: true,
|
||||
rotation: true,
|
||||
translateX: true,
|
||||
translateY: true,
|
||||
},
|
||||
};
|
||||
|
||||
function validateProps(params: Object): void {
|
||||
for (var key in params) {
|
||||
if (!PROPS_WHITELIST.hasOwnProperty(key)) {
|
||||
throw new Error(`Property '${key}' is not supported by native animated module`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateStyles(styles: Object): void {
|
||||
var STYLES_WHITELIST = PROPS_WHITELIST.style || {};
|
||||
for (var key in styles) {
|
||||
if (!STYLES_WHITELIST.hasOwnProperty(key)) {
|
||||
throw new Error(`Style property '${key}' is not supported by native animated module`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateNewNodeTag(): number {
|
||||
return __nativeAnimatedNodeTagCount++;
|
||||
}
|
||||
|
||||
function generateNewAnimationTag(): number {
|
||||
return __nativeAnimationTagCount++;
|
||||
}
|
||||
|
||||
function assertNativeAnimatedModule(): void {
|
||||
invariant(NativeAnimatedModule, 'Native animated module is not available');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
API,
|
||||
validateProps,
|
||||
validateStyles,
|
||||
generateNewNodeTag,
|
||||
generateNewAnimationTag,
|
||||
assertNativeAnimatedModule,
|
||||
};
|
||||
Reference in New Issue
Block a user