mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-03 22:48:25 +08:00
Execute Animated.js declarative animation on UIThread on Android.
Summary:This is the first from the series of PRs I'm going to be sending shorty that would let Animated.js animations to run off the JS thread (for Android only). This PR introduce a new native module that will be used for offloading animations - NativeAnimatedModule. It has a simple API that allows for animated nodes management via methods like: create/drop animated node, connect/disconnect nodes, start animation of a value node, attach/detach animated from a native view. Similarly to how we handle UIManager view hierarchy updates we create a queue of animated graph operations that are then executed on the UI thread. This isolates us from problems that may be caused by concurrent updates of animated graph while UI thread is "executing" the animation. The most important class NativeAnimatedNodesManager.java implements a management interface for animated nodes graph as well as implements a graph traversal algorithm that is run for each animation frame. For each animation frame we visit animated nodes th Closes https://github.com/facebook/react-native/pull/6466 Differential Revision: D3092739 Pulled By: astreet fb-gh-sync-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba shipit-source-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba
This commit is contained in:
committed by
Facebook Github Bot 5
parent
bd8007300f
commit
65ccdffc8d
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user