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:
Krzysztof Magiera 2016-03-24 06:18:39 -07:00 committed by Facebook Github Bot 5
parent bd8007300f
commit 65ccdffc8d
14 changed files with 1290 additions and 9 deletions

View File

@ -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 {

View 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,
};

View File

@ -0,0 +1,71 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.infer.annotation.Assertions;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Base class for all Animated.js library node types that can be created on the "native" side.
*/
/*package*/ abstract class AnimatedNode {
public static final int INITIAL_BFS_COLOR = 0;
private static final int DEFAULT_ANIMATED_NODE_CHILD_COUNT = 1;
/*package*/ @Nullable List<AnimatedNode> mChildren; /* lazy-initialized when a child is added */
/*package*/ int mActiveIncomingNodes = 0;
/*package*/ int mBFSColor = INITIAL_BFS_COLOR;
/*package*/ int mTag = -1;
public final void addChild(AnimatedNode child) {
if (mChildren == null) {
mChildren = new ArrayList<>(DEFAULT_ANIMATED_NODE_CHILD_COUNT);
}
Assertions.assertNotNull(mChildren).add(child);
child.onAttachedToNode(this);
}
public final void removeChild(AnimatedNode child) {
if (mChildren == null) {
return;
}
child.onDetachedFromNode(this);
mChildren.remove(child);
}
/**
* Subclasses may want to override this method in order to store a reference to the parent of a
* given node that can then be used to calculate current node's value in {@link #update}.
* In that case it is important to also override {@link #onDetachedFromNode} to clear that
* reference once current node gets detached.
*/
public void onAttachedToNode(AnimatedNode parent) {
}
/**
* See {@link #onAttachedToNode}
*/
public void onDetachedFromNode(AnimatedNode parent) {
}
/**
* This method will be run on each node at most once every repetition of the animation loop. It
* will be executed on a node only when all the node's parent has already been updated. Therefore
* it can be used to calculate node's value.
*/
public void update() {
}
}

View File

@ -0,0 +1,29 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.Callback;
/**
* Base class for different types of animation drivers. Can be used to implement simple time-based
* animations as well as spring based animations.
*/
/*package*/ abstract class AnimationDriver {
boolean mHasFinished = false;
ValueAnimatedNode mAnimatedValue;
Callback mEndCallback;
/**
* This method gets called in the main animation loop with a frame time passed down from the
* android choreographer callback.
*/
public abstract void runAnimationStep(long frameTimeNanos);
}

View File

@ -0,0 +1,63 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
/**
* Implementation of {@link AnimationDriver} which provides a support for simple time-based
* animations that are pre-calculate on the JS side. For each animation frame JS provides a value
* from 0 to 1 that indicates a progress of the animation at that frame.
*/
class FrameBasedAnimationDriver extends AnimationDriver {
private long mStartFrameTimeNanos = -1;
private final double[] mFrames;
private final double mToValue;
private double mFromValue;
FrameBasedAnimationDriver(ReadableMap config) {
ReadableArray frames = config.getArray("frames");
int numberOfFrames = frames.size();
mFrames = new double[numberOfFrames];
for (int i = 0; i < numberOfFrames; i++) {
mFrames[i] = frames.getDouble(i);
}
mToValue = config.getDouble("toValue");
}
@Override
public void runAnimationStep(long frameTimeNanos) {
if (mStartFrameTimeNanos < 0) {
mStartFrameTimeNanos = frameTimeNanos;
mFromValue = mAnimatedValue.mValue;
}
long timeFromStartNanos = (frameTimeNanos - mStartFrameTimeNanos);
// frames are calculated at 60FPS, to get index by a given time offset from the start of the
// animation, we take the time diff in millisecond and divide it by 60 frames per 1000ms.
int frameIndex = (int) (timeFromStartNanos / 1000000L * 60L / 1000L);
if (frameIndex < 0) {
throw new IllegalStateException("Calculated frame index should never be lower than 0");
} else if (mHasFinished) {
// nothing to do here
return;
}
double nextValue;
if (frameIndex >= mFrames.length - 1) {
// animation has completed, no more frames left
mHasFinished = true;
nextValue = mToValue;
} else {
nextValue = mFromValue + mFrames[frameIndex] * (mToValue - mFromValue);
}
mAnimatedValue.mValue = nextValue;
}
}

View File

@ -0,0 +1,269 @@
/**
* 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.
*/
package com.facebook.react.animated;
import android.support.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.OnBatchCompleteListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.GuardedChoreographerFrameCallback;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.ArrayList;
/**
* Module that exposes interface for creating and managing animated nodes on the "native" side.
*
* Animated.js library is based on a concept of a graph where nodes are values or transform
* operations (such as interpolation, addition, etc) and connection are used to describe how change
* of the value in one node can affect other nodes.
*
* Few examples of the nodes that can be created on the JS side:
* - Animated.Value is a simplest type of node with a numeric value which can be driven by an
* animation engine (spring, decay, etc) or by calling setValue on it directly from JS
* - Animated.add is a type of node that may have two or more input nodes. It outputs the sum of
* all the input node values
* - interpolate - is actually a method you can call on any node and it creates a new node that
* takes the parent node as an input and outputs its interpolated value (e.g. if you have value
* that can animate from 0 to 1 you can create interpolated node and set output range to be 0 to
* 100 and when the input node changes the output of interpolated node will multiply the values
* by 100)
*
* You can mix and chain nodes however you like and this way create nodes graph with connections
* between them.
*
* To map animated node values to view properties there is a special type of a node: AnimatedProps.
* It is created by AnimatedImplementation whenever you render Animated.View and stores a mapping
* from the view properties to the corresponding animated values (so it's actually also a node with
* connections to the value nodes).
*
* Last "special" elements of the the graph are "animation drivers". Those are objects (represented
* as a graph nodes too) that based on some criteria updates attached values every frame (we have
* few types of those, e.g., spring, timing, decay). Animation objects can be "started" and
* "stopped". Those are like "pulse generators" for the rest of the nodes graph. Those pulses then
* propagate along the graph to the children nodes up to the special node type: AnimatedProps which
* then can be used to calculate property update map for a view.
*
* This class acts as a proxy between the "native" API that can be called from JS and the main class
* that coordinates all the action: {@link NativeAnimatedNodesManager}. Since all the methods from
* {@link NativeAnimatedNodesManager} need to be called from the UI thread, we we create a queue of
* animated graph operations that is then enqueued to be executed in the UI Thread at the end of the
* batch of JS->native calls (similarily to how it's handled in {@link UIManagerModule}). This
* isolates us from the problems that may be caused by concurrent updates of animated graph while UI
* thread is "executing" the animation loop.
*/
public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
OnBatchCompleteListener, LifecycleEventListener {
private interface UIThreadOperation {
void execute(NativeAnimatedNodesManager animatedNodesManager);
}
private final Object mOperationsCopyLock = new Object();
private @Nullable GuardedChoreographerFrameCallback mAnimatedFrameCallback;
private @Nullable ReactChoreographer mReactChoreographer;
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
private volatile @Nullable ArrayList<UIThreadOperation> mReadyOperations = null;
public NativeAnimatedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public void initialize() {
// Safe to acquire choreographer here, as initialize() is invoked from UI thread.
mReactChoreographer = ReactChoreographer.getInstance();
ReactApplicationContext reactCtx = getReactApplicationContext();
UIImplementation uiImplementation =
reactCtx.getNativeModule(UIManagerModule.class).getUIImplementation();
final NativeAnimatedNodesManager nodesManager =
new NativeAnimatedNodesManager(uiImplementation);
mAnimatedFrameCallback = new GuardedChoreographerFrameCallback(reactCtx) {
@Override
protected void doFrameGuarded(final long frameTimeNanos) {
ArrayList<UIThreadOperation> operations;
synchronized (mOperationsCopyLock) {
operations = mReadyOperations;
mReadyOperations = null;
}
if (operations != null) {
for (int i = 0, size = operations.size(); i < size; i++) {
operations.get(i).execute(nodesManager);
}
}
if (nodesManager.hasActiveAnimations()) {
nodesManager.runUpdates(frameTimeNanos);
}
// TODO: Would be great to avoid adding this callback in case there are no active animations
// and no outstanding tasks on the operations queue. Apparently frame callbacks can only
// be posted from the UI thread and therefore we cannot schedule them directly from
// @ReactMethod methods
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mAnimatedFrameCallback);
}
};
reactCtx.addLifecycleEventListener(this);
}
@Override
public void onBatchComplete() {
// Note: The order of executing onBatchComplete handler (especially in terms of onBatchComplete
// from the UIManagerModule) doesn't matter as we only enqueue operations for the UI thread to
// be executed from here. Thanks to ReactChoreographer all the operations from here are going
// to be executed *after* all the operations enqueued by UIManager as the callback type that we
// use for ReactChoreographer (CallbackType.NATIVE_ANIMATED_MODULE) is run after callbacks that UIManager
// use
ArrayList<UIThreadOperation> operations = mOperations.isEmpty() ? null : mOperations;
if (operations != null) {
mOperations = new ArrayList<>();
synchronized (mOperationsCopyLock) {
if (mReadyOperations == null) {
mReadyOperations = operations;
} else {
mReadyOperations.addAll(operations);
}
}
}
}
@Override
public void onHostResume() {
enqueueFrameCallback();
}
@Override
public void onHostPause() {
clearFrameCallback();
}
@Override
public void onHostDestroy() {
// do nothing
}
@Override
public String getName() {
return "NativeAnimatedModule";
}
private void clearFrameCallback() {
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mAnimatedFrameCallback);
}
private void enqueueFrameCallback() {
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mAnimatedFrameCallback);
}
@ReactMethod
public void createAnimatedNode(final int tag, final ReadableMap config) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.createAnimatedNode(tag, config);
}
});
}
@ReactMethod
public void dropAnimatedNode(final int tag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.dropAnimatedNode(tag);
}
});
}
@ReactMethod
public void setAnimatedNodeValue(final int tag, final double value) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.setAnimatedNodeValue(tag, value);
}
});
}
@ReactMethod
public void startAnimatingNode(
final int animatedNodeTag,
final ReadableMap animationConfig,
final Callback endCallback) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.startAnimatingNode(
animatedNodeTag,
animationConfig,
endCallback);
}
});
}
@ReactMethod
public void connectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.connectAnimatedNodes(parentNodeTag, childNodeTag);
}
});
}
@ReactMethod
public void disconnectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.disconnectAnimatedNodes(parentNodeTag, childNodeTag);
}
});
}
@ReactMethod
public void connectAnimatedNodeToView(final int animatedNodeTag, final int viewTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.connectAnimatedNodeToView(animatedNodeTag, viewTag);
}
});
}
@ReactMethod
public void disconnectAnimatedNodeFromView(final int animatedNodeTag, final int viewTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag);
}
});
}
}

View File

@ -0,0 +1,338 @@
/**
* 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.
*/
package com.facebook.react.animated;
import android.util.SparseArray;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIImplementation;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Queue;
/**
* This is the main class that coordinates how native animated JS implementation drives UI changes.
*
* It 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 that might've been updated as well as their
* children that may use parent's values to update themselves. At the end of the traversal algorithm
* we expect to reach a special type of the node: PropsAnimatedNode that is then responsible for
* calculating property map which can be sent to native view hierarchy to update the view.
*
* IMPORTANT: This class should be accessed only from the UI Thread
*/
/*package*/ class NativeAnimatedNodesManager {
private final SparseArray<AnimatedNode> mAnimatedNodes = new SparseArray<>();
private final ArrayList<AnimationDriver> mActiveAnimations = new ArrayList<>();
private final ArrayList<AnimatedNode> mUpdatedNodes = new ArrayList<>();
private final UIImplementation mUIImplementation;
private int mAnimatedGraphBFSColor = 0;
public NativeAnimatedNodesManager(UIImplementation uiImplementation) {
mUIImplementation = uiImplementation;
}
/*package*/ AnimatedNode getNodeById(int id) {
return mAnimatedNodes.get(id);
}
public boolean hasActiveAnimations() {
return !mActiveAnimations.isEmpty();
}
public void createAnimatedNode(int tag, ReadableMap config) {
if (mAnimatedNodes.get(tag) != null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
" already exists");
}
String type = config.getString("type");
final AnimatedNode node;
if ("style".equals(type)) {
node = new StyleAnimatedNode(config, this);
} else if ("value".equals(type)) {
node = new ValueAnimatedNode(config);
mUpdatedNodes.add(node);
} else if ("props".equals(type)) {
node = new PropsAnimatedNode(config, this);
} else {
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
}
node.mTag = tag;
mAnimatedNodes.put(tag, node);
}
public void dropAnimatedNode(int tag) {
mAnimatedNodes.remove(tag);
}
public void setAnimatedNodeValue(int tag, double value) {
AnimatedNode node = mAnimatedNodes.get(tag);
if (node == null || !(node instanceof ValueAnimatedNode)) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
" does not exists or is not a 'value' node");
}
((ValueAnimatedNode) node).mValue = value;
mUpdatedNodes.add(node);
}
public void startAnimatingNode(
int animatedNodeTag,
ReadableMap animationConfig,
Callback endCallback) {
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + animatedNodeTag +
" does not exists");
}
if (!(node instanceof ValueAnimatedNode)) {
throw new JSApplicationIllegalArgumentException("Animated node should be of type " +
ValueAnimatedNode.class.getName());
}
String type = animationConfig.getString("type");
final AnimationDriver animation;
if ("frames".equals(type)) {
animation = new FrameBasedAnimationDriver(animationConfig);
} else {
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
}
animation.mEndCallback = endCallback;
animation.mAnimatedValue = (ValueAnimatedNode) node;
mActiveAnimations.add(animation);
}
public void connectAnimatedNodes(int parentNodeTag, int childNodeTag) {
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
if (parentNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + parentNodeTag +
" does not exists");
}
AnimatedNode childNode = mAnimatedNodes.get(childNodeTag);
if (childNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + childNodeTag +
" does not exists");
}
parentNode.addChild(childNode);
}
public void disconnectAnimatedNodes(int parentNodeTag, int childNodeTag) {
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
if (parentNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + parentNodeTag +
" does not exists");
}
AnimatedNode childNode = mAnimatedNodes.get(childNodeTag);
if (childNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + childNodeTag +
" does not exists");
}
parentNode.removeChild(childNode);
}
public void connectAnimatedNodeToView(int animatedNodeTag, int viewTag) {
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + animatedNodeTag +
" does not exists");
}
if (!(node instanceof PropsAnimatedNode)) {
throw new JSApplicationIllegalArgumentException("Animated node connected to view should be" +
"of type " + PropsAnimatedNode.class.getName());
}
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
if (propsAnimatedNode.mConnectedViewTag != -1) {
throw new JSApplicationIllegalArgumentException("Animated node " + animatedNodeTag + " is " +
"already attached to a view");
}
propsAnimatedNode.mConnectedViewTag = viewTag;
}
public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) {
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Animated node with tag " + animatedNodeTag +
" does not exists");
}
if (!(node instanceof PropsAnimatedNode)) {
throw new JSApplicationIllegalArgumentException("Animated node connected to view should be" +
"of type " + PropsAnimatedNode.class.getName());
}
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
if (propsAnimatedNode.mConnectedViewTag != viewTag) {
throw new JSApplicationIllegalArgumentException("Attempting to disconnect view that has " +
"not been connected with the given animated node");
}
propsAnimatedNode.mConnectedViewTag = -1;
}
/**
* Animation loop performs two BFSes over the graph of animated nodes. We use incremented
* {@code mAnimatedGraphBFSColor} to mark nodes as visited in each of the BFSes which saves
* additional loops for clearing "visited" states.
*
* First BFS starts with nodes that are in {@code mUpdatedNodes} (that is, their value have been
* modified from JS in the last batch of JS operations) or directly attached to an active
* animation (hence linked to objects from {@code mActiveAnimations}). In that step we calculate
* an attribute {@code mActiveIncomingNodes}. The second BFS runs in topological order over the
* sub-graph of *active* nodes. This is done by adding node to the BFS queue only if all its
* "predecessors" have already been visited.
*/
public void runUpdates(long frameTimeNanos) {
UiThreadUtil.assertOnUiThread();
int activeNodesCount = 0;
int updatedNodesCount = 0;
boolean hasFinishedAnimations = false;
// STEP 1.
// BFS over graph of nodes starting from ones from `mUpdatedNodes` and ones that are attached to
// active animations (from `mActiveAnimations)`. Update `mIncomingNodes` attribute for each node
// during that BFS. Store number of visited nodes in `activeNodesCount`. We "execute" active
// animations as a part of this step.
mAnimatedGraphBFSColor++; /* use new color */
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
// value "0" is used as an initial color for a new node, using it in BFS may cause some nodes
// to be skipped.
mAnimatedGraphBFSColor++;
}
Queue<AnimatedNode> nodesQueue = new ArrayDeque<>();
for (int i = 0; i < mUpdatedNodes.size(); i++) {
AnimatedNode node = mUpdatedNodes.get(i);
if (node.mBFSColor != mAnimatedGraphBFSColor) {
node.mBFSColor = mAnimatedGraphBFSColor;
activeNodesCount++;
nodesQueue.add(node);
}
}
for (int i = 0; i < mActiveAnimations.size(); i++) {
AnimationDriver animation = mActiveAnimations.get(i);
animation.runAnimationStep(frameTimeNanos);
AnimatedNode valueNode = animation.mAnimatedValue;
if (valueNode.mBFSColor != mAnimatedGraphBFSColor) {
valueNode.mBFSColor = mAnimatedGraphBFSColor;
activeNodesCount++;
nodesQueue.add(valueNode);
}
if (animation.mHasFinished) {
hasFinishedAnimations = true;
}
}
while (!nodesQueue.isEmpty()) {
AnimatedNode nextNode = nodesQueue.poll();
if (nextNode.mChildren != null) {
for (int i = 0; i < nextNode.mChildren.size(); i++) {
AnimatedNode child = nextNode.mChildren.get(i);
child.mActiveIncomingNodes++;
if (child.mBFSColor != mAnimatedGraphBFSColor) {
child.mBFSColor = mAnimatedGraphBFSColor;
activeNodesCount++;
nodesQueue.add(child);
}
}
}
}
// STEP 2
// BFS over the graph of active nodes in topological order -> visit node only when all its
// "predecessors" in the graph have already been visited. It is important to visit nodes in that
// order as they may often use values of their predecessors in order to calculate "next state"
// of their own. We start by determining the starting set of nodes by looking for nodes with
// `mActiveIncomingNodes = 0` (those can only be the ones that we start BFS in the previous
// step). We store number of visited nodes in this step in `updatedNodesCount`
mAnimatedGraphBFSColor++;
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
// see reasoning for this check a few lines above
mAnimatedGraphBFSColor++;
}
// find nodes with zero "incoming nodes", those can be either nodes from `mUpdatedNodes` or
// ones connected to active animations
for (int i = 0; i < mUpdatedNodes.size(); i++) {
AnimatedNode node = mUpdatedNodes.get(i);
if (node.mActiveIncomingNodes == 0 && node.mBFSColor != mAnimatedGraphBFSColor) {
node.mBFSColor = mAnimatedGraphBFSColor;
updatedNodesCount++;
nodesQueue.add(node);
}
}
for (int i = 0; i < mActiveAnimations.size(); i++) {
AnimationDriver animation = mActiveAnimations.get(i);
AnimatedNode valueNode = animation.mAnimatedValue;
if (valueNode.mActiveIncomingNodes == 0 && valueNode.mBFSColor != mAnimatedGraphBFSColor) {
valueNode.mBFSColor = mAnimatedGraphBFSColor;
updatedNodesCount++;
nodesQueue.add(valueNode);
}
}
// Run main "update" loop
while (!nodesQueue.isEmpty()) {
AnimatedNode nextNode = nodesQueue.poll();
nextNode.update();
if (nextNode instanceof PropsAnimatedNode) {
// Send property updates to native view manager
((PropsAnimatedNode) nextNode).updateView(mUIImplementation);
}
if (nextNode.mChildren != null) {
for (int i = 0; i < nextNode.mChildren.size(); i++) {
AnimatedNode child = nextNode.mChildren.get(i);
child.mActiveIncomingNodes--;
if (child.mBFSColor != mAnimatedGraphBFSColor && child.mActiveIncomingNodes == 0) {
child.mBFSColor = mAnimatedGraphBFSColor;
updatedNodesCount++;
nodesQueue.add(child);
}
}
}
}
// Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a
// cycle in animated node graph. We also take advantage of the fact that all active nodes are
// visited in the step above so that all the nodes properties `mActiveIncomingNodes` are set to
// zero
if (activeNodesCount != updatedNodesCount) {
throw new IllegalStateException("Looks like animated nodes graph has cycles, there are "
+ activeNodesCount + " but toposort visited only " + updatedNodesCount);
}
// Cleanup finished animations. Iterate over the array of animations and override ones that has
// finished, then resize `mActiveAnimations`.
if (hasFinishedAnimations) {
int dest = 0;
for (int i = 0; i < mActiveAnimations.size(); i++) {
AnimationDriver animation = mActiveAnimations.get(i);
if (!animation.mHasFinished) {
mActiveAnimations.set(dest++, animation);
} else {
WritableMap endCallbackResponse = Arguments.createMap();
endCallbackResponse.putBoolean("finished", true);
animation.mEndCallback.invoke(endCallbackResponse);
}
}
for (int i = mActiveAnimations.size() - 1; i >= dest; i--) {
mActiveAnimations.remove(i);
}
}
}
}

View File

@ -0,0 +1,73 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.HashMap;
import java.util.Map;
/**
* Animated node that represents view properties. There is a special handling logic implemented for
* the nodes of this type in {@link NativeAnimatedNodesManager} that is responsible for extracting
* a map of updated properties, which can be then passed down to the view.
*/
/*package*/ class PropsAnimatedNode extends AnimatedNode {
/*package*/ int mConnectedViewTag = -1;
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
private final Map<String, Integer> mPropMapping;
PropsAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) {
ReadableMap props = config.getMap("props");
ReadableMapKeySetIterator iter = props.keySetIterator();
mPropMapping = new HashMap<>();
while (iter.hasNextKey()) {
String propKey = iter.nextKey();
int nodeIndex = props.getInt(propKey);
mPropMapping.put(propKey, nodeIndex);
}
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
}
public final void updateView(UIImplementation uiImplementation) {
if (mConnectedViewTag == -1) {
throw new IllegalStateException("Node has not been attached to a view");
}
JavaOnlyMap propsMap = new JavaOnlyMap();
for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) {
AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
if (node == null) {
throw new IllegalArgumentException("Mapped property node does not exists");
} else if (node instanceof StyleAnimatedNode) {
((StyleAnimatedNode) node).collectViewUpdates(propsMap);
} else if (node instanceof ValueAnimatedNode) {
propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).mValue);
} else {
throw new IllegalArgumentException("Unsupported type of node used in property node " +
node.getClass());
}
}
// TODO: Reuse propsMap and stylesDiffMap objects - note that in subsequent animation steps
// for a given node most of the time we will be creating the same set of props (just with
// different values). We can take advantage on that and optimize the way we allocate property
// maps (we also know that updating view props doesn't retain a reference to the styles object).
uiImplementation.synchronouslyUpdateViewOnUIThread(
mConnectedViewTag,
new ReactStylesDiffMap(propsMap));
}
}

View File

@ -0,0 +1,52 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.HashMap;
import java.util.Map;
/**
* Native counterpart of style animated node (see AnimatedStyle class in AnimatedImplementation.js)
*/
/*package*/ class StyleAnimatedNode extends AnimatedNode {
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
private final Map<String, Integer> mPropMapping;
StyleAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) {
ReadableMap style = config.getMap("style");
ReadableMapKeySetIterator iter = style.keySetIterator();
mPropMapping = new HashMap<>();
while (iter.hasNextKey()) {
String propKey = iter.nextKey();
int nodeIndex = style.getInt(propKey);
mPropMapping.put(propKey, nodeIndex);
}
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
}
public void collectViewUpdates(JavaOnlyMap propsMap) {
for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) {
AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
if (node == null) {
throw new IllegalArgumentException("Mapped style node does not exists");
} else if (node instanceof ValueAnimatedNode) {
propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).mValue);
} else {
throw new IllegalArgumentException("Unsupported type of node used in property node " +
node.getClass());
}
}
}
}

View File

@ -0,0 +1,25 @@
/**
* 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.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.ReadableMap;
/**
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
* library.
*/
class ValueAnimatedNode extends AnimatedNode {
/*package*/ double mValue = Double.NaN;
ValueAnimatedNode(ReadableMap config) {
mValue = config.getDouble("value");
}
}

View File

@ -37,10 +37,15 @@ public class ReactChoreographer {
*/
DISPATCH_UI(1),
/**
* For use by {@link com.facebook.react.uimanager.animation.NativeAnimatedModule}
*/
NATIVE_ANIMATED_MODULE(2),
/**
* Events that make JS do things.
*/
TIMERS_EVENTS(2),
TIMERS_EVENTS(3),
;
private final int mOrder;

View File

@ -21,6 +21,7 @@ import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
import com.facebook.react.uimanager.events.EventDispatcher;
@ -79,6 +80,10 @@ public class UIImplementation {
return mViewManagers.get(className);
}
/*package*/ UIViewOperationQueue getUIViewOperationQueue() {
return mOperationsQueue;
}
/**
* Registers a root node with a given tag, size and ThemedReactContext
* and adds it to a node registry.
@ -179,6 +184,17 @@ public class UIImplementation {
}
}
/**
* Used by native animated module to bypass the process of updating the values through the shadow
* view hierarchy. This method will directly update native views, which means that updates for
* layout-related propertied won't be handled properly.
* Make sure you know what you're doing before calling this method :)
*/
public void synchronouslyUpdateViewOnUIThread(int tag, ReactStylesDiffMap props) {
UiThreadUtil.assertOnUiThread();
mOperationsQueue.getNativeViewHierarchyManager().updateProperties(tag, props);
}
protected void handleUpdateView(
ReactShadowNode cssNode,
String className,

View File

@ -83,6 +83,14 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements
reactContext.addLifecycleEventListener(this);
}
/**
* This method gives an access to the {@link UIImplementation} object that can be used to execute
* operations on the view hierarchy.
*/
public UIImplementation getUIImplementation() {
return mUIImplementation;
}
@Override
public String getName() {
return "RKUIManager";

View File

@ -512,6 +512,10 @@ public class UIViewOperationQueue {
mReactApplicationContext = reactContext;
}
/*package*/ NativeViewHierarchyManager getNativeViewHierarchyManager() {
return mNativeViewHierarchyManager;
}
public void setViewHierarchyUpdateDebugListener(
@Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) {
mViewHierarchyUpdateDebugListener = listener;