diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index ee6651b26..f38a92834 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -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 { 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 { diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js new file mode 100644 index 000000000..c85b89f6b --- /dev/null +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -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, +}; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.java new file mode 100644 index 000000000..61e02b105 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.java @@ -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 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() { + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java new file mode 100644 index 000000000..f2426c79c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java @@ -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); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java b/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java new file mode 100644 index 000000000..f697e98f8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java @@ -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; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java new file mode 100644 index 000000000..bdb833f27 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -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 mOperations = new ArrayList<>(); + private volatile @Nullable ArrayList 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 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 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); + } + }); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java new file mode 100644 index 000000000..185716b36 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -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 mAnimatedNodes = new SparseArray<>(); + private final ArrayList mActiveAnimations = new ArrayList<>(); + private final ArrayList 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 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); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java new file mode 100644 index 000000000..c3d9667ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -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 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 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)); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java new file mode 100644 index 000000000..7841fd215 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java @@ -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 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 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()); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java new file mode 100644 index 000000000..e02d90ad7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java @@ -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"); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java index 106356c89..9109ca849 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java @@ -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; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index b407c416a..443c8a2a5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -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, diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index ddeb94e6d..b3109e66b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -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"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index b3b931e0f..114417c95 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -512,6 +512,10 @@ public class UIViewOperationQueue { mReactApplicationContext = reactContext; } + /*package*/ NativeViewHierarchyManager getNativeViewHierarchyManager() { + return mNativeViewHierarchyManager; + } + public void setViewHierarchyUpdateDebugListener( @Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) { mViewHierarchyUpdateDebugListener = listener;