/**
* 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 javax.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
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.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.GuardedChoreographerFrameCallback;
import com.facebook.react.uimanager.ReactChoreographer;
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.
*/
@ReactModule(name = "NativeAnimatedModule")
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();
UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
final NativeAnimatedNodesManager nodesManager = new NativeAnimatedNodesManager(uiManager);
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 uses.
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 startListeningToAnimatedNodeValue(final int tag) {
final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() {
public void onValueUpdate(double value) {
WritableMap onAnimatedValueData = Arguments.createMap();
onAnimatedValueData.putInt("tag", tag);
onAnimatedValueData.putDouble("value", value);
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onAnimatedValueUpdate", onAnimatedValueData);
}
};
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
}
});
}
@ReactMethod
public void stopListeningToAnimatedNodeValue(final int tag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.stopListeningToAnimatedNodeValue(tag);
}
});
}
@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 setAnimatedNodeOffset(final int tag, final double value) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.setAnimatedNodeOffset(tag, value);
}
});
}
@ReactMethod
public void flattenAnimatedNodeOffset(final int tag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.flattenAnimatedNodeOffset(tag);
}
});
}
@ReactMethod
public void extractAnimatedNodeOffset(final int tag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.extractAnimatedNodeOffset(tag);
}
});
}
@ReactMethod
public void startAnimatingNode(
final int animationId,
final int animatedNodeTag,
final ReadableMap animationConfig,
final Callback endCallback) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.startAnimatingNode(
animationId,
animatedNodeTag,
animationConfig,
endCallback);
}
});
}
@ReactMethod
public void stopAnimation(final int animationId) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.stopAnimation(animationId);
}
});
}
@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);
}
});
}
@ReactMethod
public void addAnimatedEventToView(final int viewTag, final String eventName, final ReadableMap eventMapping) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping);
}
});
}
@ReactMethod
public void removeAnimatedEventFromView(final int viewTag, final String eventName) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName);
}
});
}
}