Files
react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
T
Pieter De Baets fe8f0fb09f Apply Animated initialProps handling to Fabric only (#34927)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/34927

The changes made in D36902220 (https://github.com/facebook/react-native/commit/a04195167bbd8f27c6141c0239a61a345cac5a88) and D36958882 (https://github.com/facebook/react-native/commit/d8c25ca1b62df2b93f70bbb1f7b379643ab9ccd4) attempted to reduce flickering and consistency issues when using Animated.

In the old renderer, we explicitly reset all animated props, and wait for the subsequent React commit to set the props to the right state, but if `initialProps` are used, the React reconciliation may not be able to identify the prop-update is required and will leave out the value. This behaviour is different in the new renderer, where we do not explicitly `restoreDefaultValues` on detaching the animated node, and instead rely on the latest state being correct(?).

Changelog:
[General][Fixed] Stop styles from being reset when detaching Animated.Values in old renderer

Fixes #34665

Reviewed By: rshest

Differential Revision: D40194072

fbshipit-source-id: 1b3fb1d1f4a39036a501a8a21e57002035dd5659

# Conflicts:
#	Libraries/Animated/createAnimatedComponent.js
2022-10-11 13:25:39 +01:00

1205 lines
44 KiB
Java

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.animated;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.facebook.common.logging.FLog;
import com.facebook.fbreact.specs.NativeAnimatedModuleSpec;
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.ReactApplicationContext;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.UIManagerListener;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.uimanager.GuardedFrameCallback;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.common.UIManagerType;
import com.facebook.react.uimanager.common.ViewUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
/**
* Module that exposes interface for creating and managing animated nodes on the "native" side.
*
* <p>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.
*
* <p>Few examples of the nodes that can be created on the JS side:
*
* <ul>
* <li>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
* <li>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
* <li>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)
* </ul>
*
* <p>You can mix and chain nodes however you like and this way create nodes graph with connections
* between them.
*
* <p>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).
*
* <p>Last "special" elements of 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.
*
* <p>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 (similarly 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.NAME)
public class NativeAnimatedModule extends NativeAnimatedModuleSpec
implements LifecycleEventListener, UIManagerListener {
public static final String NAME = "NativeAnimatedModule";
public static final boolean ANIMATED_MODULE_DEBUG = false;
// For `queueAndExecuteBatchedOperations`
private enum BatchExecutionOpCodes {
OP_CODE_CREATE_ANIMATED_NODE(1),
OP_CODE_UPDATE_ANIMATED_NODE_CONFIG(2),
OP_CODE_GET_VALUE(3),
OP_START_LISTENING_TO_ANIMATED_NODE_VALUE(4),
OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE(5),
OP_CODE_CONNECT_ANIMATED_NODES(6),
OP_CODE_DISCONNECT_ANIMATED_NODES(7),
OP_CODE_START_ANIMATING_NODE(8),
OP_CODE_STOP_ANIMATION(9),
OP_CODE_SET_ANIMATED_NODE_VALUE(10),
OP_CODE_SET_ANIMATED_NODE_OFFSET(11),
OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET(12),
OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET(13),
OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW(14),
OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW(15),
OP_CODE_RESTORE_DEFAULT_VALUES(16),
OP_CODE_DROP_ANIMATED_NODE(17),
OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW(18),
OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW(19),
OP_CODE_ADD_LISTENER(20), // ios only
OP_CODE_REMOVE_LISTENERS(21); // ios only
private static BatchExecutionOpCodes[] valueMap = null;
private final int value;
private BatchExecutionOpCodes(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
public static BatchExecutionOpCodes fromId(int id) {
if (BatchExecutionOpCodes.valueMap == null) {
BatchExecutionOpCodes.valueMap = BatchExecutionOpCodes.values();
}
// Enum values are 1-indexed, but the value array is 0-indexed
return BatchExecutionOpCodes.valueMap[id - 1];
}
}
private abstract class UIThreadOperation {
abstract void execute(NativeAnimatedNodesManager animatedNodesManager);
long mBatchNumber = -1;
public void setBatchNumber(long batchNumber) {
mBatchNumber = batchNumber;
}
public long getBatchNumber() {
return mBatchNumber;
}
}
private class ConcurrentOperationQueue {
private final Queue<UIThreadOperation> mQueue = new ConcurrentLinkedQueue<>();
@Nullable private UIThreadOperation mPeekedOperation = null;
private boolean mSynchronizedAccess = false;
@AnyThread
boolean isEmpty() {
return mQueue.isEmpty() && mPeekedOperation == null;
}
void setSynchronizedAccess(boolean isSynchronizedAccess) {
mSynchronizedAccess = isSynchronizedAccess;
}
@AnyThread
void add(UIThreadOperation operation) {
if (mSynchronizedAccess) {
synchronized (this) {
mQueue.add(operation);
}
} else {
mQueue.add(operation);
}
}
@UiThread
void executeBatch(long maxBatchNumber, NativeAnimatedNodesManager nodesManager) {
List<UIThreadOperation> operations;
if (mSynchronizedAccess) {
synchronized (this) {
operations = drainQueueIntoList(maxBatchNumber);
}
} else {
operations = drainQueueIntoList(maxBatchNumber);
}
if (operations != null) {
for (UIThreadOperation operation : operations) {
operation.execute(nodesManager);
}
}
}
@UiThread
private @Nullable List<UIThreadOperation> drainQueueIntoList(long maxBatchNumber) {
if (isEmpty()) {
return null;
}
List<UIThreadOperation> operations = new ArrayList<>();
while (true) {
// Due to a race condition, we manually "carry-over" a polled item from previous batch
// instead of peeking the queue itself for consistency.
// TODO(T112522554): Clean up the queue access
if (mPeekedOperation != null) {
if (mPeekedOperation.getBatchNumber() > maxBatchNumber) {
break;
}
operations.add(mPeekedOperation);
mPeekedOperation = null;
}
UIThreadOperation polledOperation = mQueue.poll();
if (polledOperation == null) {
// This is the same as mQueue.isEmpty()
break;
}
if (polledOperation.getBatchNumber() > maxBatchNumber) {
// Because the operation is already retrieved from the queue, there's no way of placing it
// back as the head element, so we remember it manually here
mPeekedOperation = polledOperation;
break;
}
operations.add(polledOperation);
}
return operations;
}
}
@NonNull private final GuardedFrameCallback mAnimatedFrameCallback;
private final ReactChoreographer mReactChoreographer;
@NonNull private final ConcurrentOperationQueue mOperations = new ConcurrentOperationQueue();
@NonNull private final ConcurrentOperationQueue mPreOperations = new ConcurrentOperationQueue();
private final AtomicReference<NativeAnimatedNodesManager> mNodesManager = new AtomicReference<>();
private boolean mBatchingControlledByJS = false; // TODO T71377544: delete
private volatile long mCurrentFrameNumber; // TODO T71377544: delete
private volatile long mCurrentBatchNumber;
private boolean mInitializedForFabric = false;
private boolean mInitializedForNonFabric = false;
private @UIManagerType int mUIManagerType = UIManagerType.DEFAULT;
private int mNumFabricAnimations = 0;
private int mNumNonFabricAnimations = 0;
public NativeAnimatedModule(ReactApplicationContext reactContext) {
super(reactContext);
mReactChoreographer = ReactChoreographer.getInstance();
mAnimatedFrameCallback =
new GuardedFrameCallback(reactContext) {
@Override
protected void doFrameGuarded(final long frameTimeNanos) {
try {
NativeAnimatedNodesManager nodesManager = getNodesManager();
if (nodesManager != null && nodesManager.hasActiveAnimations()) {
nodesManager.runUpdates(frameTimeNanos);
}
// This is very unlikely to ever be hit.
if (nodesManager == null && mReactChoreographer == null) {
return;
}
// 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 other threads.
Assertions.assertNotNull(mReactChoreographer)
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mAnimatedFrameCallback);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
};
// If shipping this flag, make sure to migrate to non-concurrent queue for efficiency
mOperations.setSynchronizedAccess(ReactFeatureFlags.enableSynchronizationForAnimated);
mPreOperations.setSynchronizedAccess(ReactFeatureFlags.enableSynchronizationForAnimated);
}
@Override
public void initialize() {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext.addLifecycleEventListener(this);
}
}
@Override
public void onHostResume() {
enqueueFrameCallback();
}
private void addOperation(UIThreadOperation operation) {
operation.setBatchNumber(mCurrentBatchNumber);
mOperations.add(operation);
}
private void addUnbatchedOperation(UIThreadOperation operation) {
operation.setBatchNumber(-1);
mOperations.add(operation);
}
private void addPreOperation(UIThreadOperation operation) {
operation.setBatchNumber(mCurrentBatchNumber);
mPreOperations.add(operation);
}
// For FabricUIManager only
@Override
public void didScheduleMountItems(UIManager uiManager) {
mCurrentFrameNumber++;
}
// For FabricUIManager only
@Override
@UiThread
public void didDispatchMountItems(UIManager uiManager) {
if (mUIManagerType != UIManagerType.FABRIC) {
return;
}
long batchNumber = mCurrentBatchNumber - 1;
// TODO T71377544: delete this when the JS method is confirmed safe
if (!mBatchingControlledByJS) {
// The problem we're trying to solve here: we could be in the middle of queueing
// a batch of related animation operations when Fabric flushes a batch of MountItems.
// It's visually bad if we execute half of the animation ops and then wait another frame
// (or more) to execute the rest.
// See mFrameNumber. If the dispatchedFrameNumber drifts too far - that
// is, if no MountItems are scheduled for a while, which can happen if a tree
// is committed but there are no changes - bring these counts back in sync and
// execute any queued operations. This number is arbitrary, but we want it low
// enough that the user shouldn't be able to see this delay in most cases.
mCurrentFrameNumber++;
if ((mCurrentFrameNumber - mCurrentBatchNumber) > 2) {
mCurrentBatchNumber = mCurrentFrameNumber;
batchNumber = mCurrentBatchNumber;
}
}
mPreOperations.executeBatch(batchNumber, getNodesManager());
mOperations.executeBatch(batchNumber, getNodesManager());
}
// For non-FabricUIManager only
@Override
@UiThread
public void willDispatchViewUpdates(final UIManager uiManager) {
if (mOperations.isEmpty() && mPreOperations.isEmpty()) {
return;
}
if (mUIManagerType == UIManagerType.FABRIC) {
return;
}
final long frameNo = mCurrentBatchNumber++;
UIBlock preOperationsUIBlock =
new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
mPreOperations.executeBatch(frameNo, getNodesManager());
}
};
UIBlock operationsUIBlock =
new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
mOperations.executeBatch(frameNo, getNodesManager());
}
};
assert (uiManager instanceof UIManagerModule);
UIManagerModule uiManagerModule = (UIManagerModule) uiManager;
uiManagerModule.prependUIBlock(preOperationsUIBlock);
uiManagerModule.addUIBlock(operationsUIBlock);
}
@Override
public void onHostPause() {
clearFrameCallback();
}
@Override
public void onHostDestroy() {
// Is it possible for onHostDestroy to be called without a corresponding onHostPause?
clearFrameCallback();
}
@Override
public String getName() {
return NAME;
}
/**
* Returns a {@link NativeAnimatedNodesManager}, either the existing instance or a new one. Will
* return null if and only if the {@link ReactApplicationContext} is also null.
*
* @return {@link NativeAnimatedNodesManager}
*/
@Nullable
public NativeAnimatedNodesManager getNodesManager() {
if (mNodesManager.get() == null) {
ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
mNodesManager.compareAndSet(null, new NativeAnimatedNodesManager(reactApplicationContext));
}
}
return mNodesManager.get();
}
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);
}
@VisibleForTesting
public void setNodesManager(NativeAnimatedNodesManager nodesManager) {
mNodesManager.set(nodesManager);
}
/**
* Given a viewTag, detect if we're running in Fabric or non-Fabric and attach an event listener
* to the correct UIManager, if necessary. This is expected to only be called from the native
* module thread, and not concurrently.
*
* @param viewTag
*/
private void initializeLifecycleEventListenersForViewTag(final int viewTag) {
mUIManagerType = ViewUtil.getUIManagerType(viewTag);
if (mUIManagerType == UIManagerType.FABRIC) {
mNumFabricAnimations++;
} else {
mNumNonFabricAnimations++;
}
NativeAnimatedNodesManager nodesManager = getNodesManager();
if (nodesManager != null) {
nodesManager.initializeEventListenerForUIManagerType(mUIManagerType);
} else {
ReactSoftExceptionLogger.logSoftException(
NAME,
new RuntimeException(
"initializeLifecycleEventListenersForViewTag could not get NativeAnimatedNodesManager"));
}
// Subscribe to UIManager (Fabric or non-Fabric) lifecycle events if we haven't yet
if (mUIManagerType == UIManagerType.FABRIC ? mInitializedForFabric : mInitializedForNonFabric) {
return;
}
ReactApplicationContext reactApplicationContext = getReactApplicationContext();
if (reactApplicationContext != null) {
@Nullable
UIManager uiManager = UIManagerHelper.getUIManager(reactApplicationContext, mUIManagerType);
if (uiManager != null) {
uiManager.addUIManagerEventListener(this);
if (mUIManagerType == UIManagerType.FABRIC) {
mInitializedForFabric = true;
} else {
mInitializedForNonFabric = true;
}
}
}
}
/**
* Given a viewTag and the knowledge that a "disconnect" or "stop"-type imperative command is
* being executed, decrement the number of inflight animations and possibly switch UIManager
* modes.
*
* @param viewTag
*/
private void decrementInFlightAnimationsForViewTag(final int viewTag) {
@UIManagerType int animationManagerType = ViewUtil.getUIManagerType(viewTag);
if (animationManagerType == UIManagerType.FABRIC) {
mNumFabricAnimations--;
} else {
mNumNonFabricAnimations--;
}
// Should we switch to a different animation mode?
// This can be useful when navigating between Fabric and non-Fabric screens:
// If there are ongoing Fabric animations from a previous screen,
// and we tear down the current non-Fabric screen, we should expect
// the animation mode to switch back - and vice-versa.
if (mNumNonFabricAnimations == 0
&& mNumFabricAnimations > 0
&& mUIManagerType != UIManagerType.FABRIC) {
mUIManagerType = UIManagerType.FABRIC;
} else if (mNumFabricAnimations == 0
&& mNumNonFabricAnimations > 0
&& mUIManagerType != UIManagerType.DEFAULT) {
mUIManagerType = UIManagerType.DEFAULT;
}
}
@Override
public void startOperationBatch() {
mBatchingControlledByJS = true;
mCurrentBatchNumber++;
}
@Override
public void finishOperationBatch() {
mBatchingControlledByJS = true;
mCurrentBatchNumber++;
}
@Override
public void createAnimatedNode(final double tagDouble, final ReadableMap config) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME, "queue createAnimatedNode: " + tag + " config: " + config.toHashMap().toString());
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute createAnimatedNode: "
+ tag
+ " config: "
+ config.toHashMap().toString());
}
animatedNodesManager.createAnimatedNode(tag, config);
}
});
}
@Override
public void updateAnimatedNodeConfig(final double tagDouble, final ReadableMap config) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue updateAnimatedNodeConfig: " + tag + " config: " + config.toHashMap().toString());
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute updateAnimatedNodeConfig: "
+ tag
+ " config: "
+ config.toHashMap().toString());
}
animatedNodesManager.updateAnimatedNodeConfig(tag, config);
}
});
}
@Override
public void startListeningToAnimatedNodeValue(final double tagDouble) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue startListeningToAnimatedNodeValue: " + tag);
}
final AnimatedNodeValueListener listener =
new AnimatedNodeValueListener() {
public void onValueUpdate(double value) {
WritableMap onAnimatedValueData = Arguments.createMap();
onAnimatedValueData.putInt("tag", tag);
onAnimatedValueData.putDouble("value", value);
ReactApplicationContext reactApplicationContext =
getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onAnimatedValueUpdate", onAnimatedValueData);
}
}
};
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute startListeningToAnimatedNodeValue: " + tag);
}
animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
}
});
}
@Override
public void stopListeningToAnimatedNodeValue(final double tagDouble) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue stopListeningToAnimatedNodeValue: " + tag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute stopListeningToAnimatedNodeValue: " + tag);
}
animatedNodesManager.stopListeningToAnimatedNodeValue(tag);
}
});
}
@Override
public void dropAnimatedNode(final double tagDouble) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue dropAnimatedNode: " + tag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute dropAnimatedNode: " + tag);
}
animatedNodesManager.dropAnimatedNode(tag);
}
});
}
@Override
public void setAnimatedNodeValue(final double tagDouble, final double value) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue setAnimatedNodeValue: " + tag + " value: " + value);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute setAnimatedNodeValue: " + tag + " value: " + value);
}
animatedNodesManager.setAnimatedNodeValue(tag, value);
}
});
}
@Override
public void setAnimatedNodeOffset(final double tagDouble, final double value) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue setAnimatedNodeOffset: " + tag + " offset: " + value);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute setAnimatedNodeOffset: " + tag + " offset: " + value);
}
animatedNodesManager.setAnimatedNodeOffset(tag, value);
}
});
}
@Override
public void flattenAnimatedNodeOffset(final double tagDouble) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue flattenAnimatedNodeOffset: " + tag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute flattenAnimatedNodeOffset: " + tag);
}
animatedNodesManager.flattenAnimatedNodeOffset(tag);
}
});
}
@Override
public void extractAnimatedNodeOffset(final double tagDouble) {
final int tag = (int) tagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue extractAnimatedNodeOffset: " + tag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute extractAnimatedNodeOffset: " + tag);
}
animatedNodesManager.extractAnimatedNodeOffset(tag);
}
});
}
@Override
public void startAnimatingNode(
final double animationIdDouble,
final double animatedNodeTagDouble,
final ReadableMap animationConfig,
final Callback endCallback) {
final int animationId = (int) animationIdDouble;
final int animatedNodeTag = (int) animatedNodeTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag);
}
addUnbatchedOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag);
}
animatedNodesManager.startAnimatingNode(
animationId, animatedNodeTag, animationConfig, endCallback);
}
});
}
@Override
public void stopAnimation(final double animationIdDouble) {
final int animationId = (int) animationIdDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue stopAnimation: ID: " + animationId);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute stopAnimation: ID: " + animationId);
}
animatedNodesManager.stopAnimation(animationId);
}
});
}
@Override
public void connectAnimatedNodes(
final double parentNodeTagDouble, final double childNodeTagDouble) {
final int parentNodeTag = (int) parentNodeTagDouble;
final int childNodeTag = (int) childNodeTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME, "queue connectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute connectAnimatedNodes: parent: "
+ parentNodeTag
+ " child: "
+ childNodeTag);
}
animatedNodesManager.connectAnimatedNodes(parentNodeTag, childNodeTag);
}
});
}
@Override
public void disconnectAnimatedNodes(
final double parentNodeTagDouble, final double childNodeTagDouble) {
final int parentNodeTag = (int) parentNodeTagDouble;
final int childNodeTag = (int) childNodeTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue disconnectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag);
}
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute disconnectAnimatedNodes: parent: "
+ parentNodeTag
+ " child: "
+ childNodeTag);
}
animatedNodesManager.disconnectAnimatedNodes(parentNodeTag, childNodeTag);
}
});
}
@Override
public void connectAnimatedNodeToView(
final double animatedNodeTagDouble, final double viewTagDouble) {
final int animatedNodeTag = (int) animatedNodeTagDouble;
final int viewTag = (int) viewTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue connectAnimatedNodeToView: animatedNodeTag: "
+ animatedNodeTag
+ " viewTag: "
+ viewTag);
}
initializeLifecycleEventListenersForViewTag(viewTag);
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute connectAnimatedNodeToView: animatedNodeTag: "
+ animatedNodeTag
+ " viewTag: "
+ viewTag);
}
animatedNodesManager.connectAnimatedNodeToView(animatedNodeTag, viewTag);
}
});
}
@Override
public void disconnectAnimatedNodeFromView(
final double animatedNodeTagDouble, final double viewTagDouble) {
final int animatedNodeTag = (int) animatedNodeTagDouble;
final int viewTag = (int) viewTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue disconnectAnimatedNodeFromView: " + animatedNodeTag + " viewTag: " + viewTag);
}
decrementInFlightAnimationsForViewTag(viewTag);
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute disconnectAnimatedNodeFromView: "
+ animatedNodeTag
+ " viewTag: "
+ viewTag);
}
animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag);
}
});
}
@Override
public void restoreDefaultValues(final double animatedNodeTagDouble) {
final int animatedNodeTag = (int) animatedNodeTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "queue restoreDefaultValues: " + animatedNodeTag);
}
addPreOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(NAME, "execute restoreDefaultValues: " + animatedNodeTag);
}
animatedNodesManager.restoreDefaultValues(animatedNodeTag);
}
});
}
@Override
public void addAnimatedEventToView(
final double viewTagDouble, final String eventName, final ReadableMap eventMapping) {
final int viewTag = (int) viewTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue addAnimatedEventToView: "
+ viewTag
+ " eventName: "
+ eventName
+ " eventMapping: "
+ eventMapping.toHashMap().toString());
}
initializeLifecycleEventListenersForViewTag(viewTag);
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute addAnimatedEventToView: "
+ viewTag
+ " eventName: "
+ eventName
+ " eventMapping: "
+ eventMapping.toHashMap().toString());
}
animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping);
}
});
}
@Override
public void removeAnimatedEventFromView(
final double viewTagDouble, final String eventName, final double animatedValueTagDouble) {
final int viewTag = (int) viewTagDouble;
final int animatedValueTag = (int) animatedValueTagDouble;
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"queue removeAnimatedEventFromView: viewTag: "
+ viewTag
+ " eventName: "
+ eventName
+ " animatedValueTag: "
+ animatedValueTag);
}
decrementInFlightAnimationsForViewTag(viewTag);
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
if (ANIMATED_MODULE_DEBUG) {
FLog.d(
NAME,
"execute removeAnimatedEventFromView: viewTag: "
+ viewTag
+ " eventName: "
+ eventName
+ " animatedValueTag: "
+ animatedValueTag);
}
animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName, animatedValueTag);
}
});
}
@Override
public void addListener(String eventName) {
// iOS only
}
@Override
public void removeListeners(double count) {
// iOS only
}
@Override
public void getValue(final double animatedValueNodeTagDouble, final Callback callback) {
final int animatedValueNodeTag = (int) animatedValueNodeTagDouble;
addOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.getValue(animatedValueNodeTag, callback);
}
});
}
@Override
public void invalidate() {
ReactApplicationContext context = getReactApplicationContextIfActiveOrWarn();
if (context != null) {
context.removeLifecycleEventListener(this);
}
}
/**
* This is a currently-experimental method that allows JS to queue and immediately execute many
* instructions at once. Since we make 1 JNI/JSI call instead of N, this should significantly
* improve performance.
*
* <p>The arguments operate as a byte buffer. All integer command IDs and any args are packed into
* opsAndArgs.
*
* <p>For the getValue callback: since this is batched, we accumulate a list of all requested
* values, in order, and call the callback once at the end (if present) with the list of requested
* values.
*/
@Override
public void queueAndExecuteBatchedOperations(final ReadableArray opsAndArgs) {
final int opBufferSize = opsAndArgs.size();
if (ANIMATED_MODULE_DEBUG) {
FLog.e(NAME, "queueAndExecuteBatchedOperations: opBufferSize: " + opBufferSize);
}
// This block of code is unfortunate and should be refactored - we just want to
// extract the ViewTags in the ReadableArray to mark animations on views as being enabled.
// We only do this for initializing animations on views - disabling animations on views
// happens later, when the disconnect/stop operations are actually executed.
for (int i = 0; i < opBufferSize; ) {
BatchExecutionOpCodes command = BatchExecutionOpCodes.fromId(opsAndArgs.getInt(i++));
switch (command) {
case OP_CODE_GET_VALUE:
case OP_START_LISTENING_TO_ANIMATED_NODE_VALUE:
case OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE:
case OP_CODE_STOP_ANIMATION:
case OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET:
case OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET:
case OP_CODE_RESTORE_DEFAULT_VALUES:
case OP_CODE_DROP_ANIMATED_NODE:
case OP_CODE_ADD_LISTENER:
case OP_CODE_REMOVE_LISTENERS:
i++;
break;
case OP_CODE_CREATE_ANIMATED_NODE:
case OP_CODE_UPDATE_ANIMATED_NODE_CONFIG:
case OP_CODE_CONNECT_ANIMATED_NODES:
case OP_CODE_DISCONNECT_ANIMATED_NODES:
case OP_CODE_SET_ANIMATED_NODE_VALUE:
case OP_CODE_SET_ANIMATED_NODE_OFFSET:
case OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW:
i += 2;
break;
case OP_CODE_START_ANIMATING_NODE:
case OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW:
i += 3;
break;
case OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW:
i++; // tag
initializeLifecycleEventListenersForViewTag(opsAndArgs.getInt(i++)); // viewTag
break;
case OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW:
initializeLifecycleEventListenersForViewTag(opsAndArgs.getInt(i++)); // viewTag
i++; // eventName
i++; // eventMapping
break;
default:
throw new IllegalArgumentException(
"Batch animation execution op: fetching viewTag: unknown op code");
}
}
// Batching happens inside this operation - so signal to the thread loop that
// this operation should be executed as soon as possible, "unbatched" with other
// UIThreadOperations
startOperationBatch();
addUnbatchedOperation(
new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
ReactApplicationContext reactApplicationContext =
getReactApplicationContextIfActiveOrWarn();
int viewTag = -1;
for (int i = 0; i < opBufferSize; ) {
BatchExecutionOpCodes command = BatchExecutionOpCodes.fromId(opsAndArgs.getInt(i++));
switch (command) {
case OP_CODE_CREATE_ANIMATED_NODE:
animatedNodesManager.createAnimatedNode(
opsAndArgs.getInt(i++), opsAndArgs.getMap(i++));
break;
case OP_CODE_UPDATE_ANIMATED_NODE_CONFIG:
animatedNodesManager.updateAnimatedNodeConfig(
opsAndArgs.getInt(i++), opsAndArgs.getMap(i++));
break;
case OP_CODE_GET_VALUE:
animatedNodesManager.getValue(opsAndArgs.getInt(i++), null);
break;
case OP_START_LISTENING_TO_ANIMATED_NODE_VALUE:
final int tag = opsAndArgs.getInt(i++);
final AnimatedNodeValueListener listener =
new AnimatedNodeValueListener() {
public void onValueUpdate(double value) {
WritableMap onAnimatedValueData = Arguments.createMap();
onAnimatedValueData.putInt("tag", tag);
onAnimatedValueData.putDouble("value", value);
ReactApplicationContext reactApplicationContext =
getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onAnimatedValueUpdate", onAnimatedValueData);
}
}
};
animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
break;
case OP_STOP_LISTENING_TO_ANIMATED_NODE_VALUE:
animatedNodesManager.stopListeningToAnimatedNodeValue(opsAndArgs.getInt(i++));
break;
case OP_CODE_CONNECT_ANIMATED_NODES:
animatedNodesManager.connectAnimatedNodes(
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
break;
case OP_CODE_DISCONNECT_ANIMATED_NODES:
animatedNodesManager.disconnectAnimatedNodes(
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
break;
case OP_CODE_START_ANIMATING_NODE:
animatedNodesManager.startAnimatingNode(
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++), opsAndArgs.getMap(i++), null);
break;
case OP_CODE_STOP_ANIMATION:
animatedNodesManager.stopAnimation(opsAndArgs.getInt(i++));
break;
case OP_CODE_SET_ANIMATED_NODE_VALUE:
animatedNodesManager.setAnimatedNodeValue(
opsAndArgs.getInt(i++), opsAndArgs.getDouble(i++));
break;
case OP_CODE_SET_ANIMATED_NODE_OFFSET:
animatedNodesManager.setAnimatedNodeValue(
opsAndArgs.getInt(i++), opsAndArgs.getDouble(i++));
break;
case OP_CODE_FLATTEN_ANIMATED_NODE_OFFSET:
animatedNodesManager.flattenAnimatedNodeOffset(opsAndArgs.getInt(i++));
break;
case OP_CODE_EXTRACT_ANIMATED_NODE_OFFSET:
animatedNodesManager.extractAnimatedNodeOffset(opsAndArgs.getInt(i++));
break;
case OP_CODE_CONNECT_ANIMATED_NODE_TO_VIEW:
animatedNodesManager.connectAnimatedNodeToView(
opsAndArgs.getInt(i++), opsAndArgs.getInt(i++));
break;
case OP_CODE_DISCONNECT_ANIMATED_NODE_FROM_VIEW:
int animatedNodeTag = opsAndArgs.getInt(i++);
viewTag = opsAndArgs.getInt(i++);
decrementInFlightAnimationsForViewTag(viewTag);
animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag);
break;
case OP_CODE_RESTORE_DEFAULT_VALUES:
animatedNodesManager.restoreDefaultValues(opsAndArgs.getInt(i++));
break;
case OP_CODE_DROP_ANIMATED_NODE:
animatedNodesManager.dropAnimatedNode(opsAndArgs.getInt(i++));
break;
case OP_CODE_ADD_ANIMATED_EVENT_TO_VIEW:
animatedNodesManager.addAnimatedEventToView(
opsAndArgs.getInt(i++), opsAndArgs.getString(i++), opsAndArgs.getMap(i++));
break;
case OP_CODE_REMOVE_ANIMATED_EVENT_FROM_VIEW:
viewTag = opsAndArgs.getInt(i++);
decrementInFlightAnimationsForViewTag(viewTag);
animatedNodesManager.removeAnimatedEventFromView(
viewTag, opsAndArgs.getString(i++), opsAndArgs.getInt(i++));
break;
case OP_CODE_ADD_LISTENER:
case OP_CODE_REMOVE_LISTENERS:
i++;
// ios only, do nothing on android besides incrementing the arg counter
break;
default:
throw new IllegalArgumentException(
"Batch animation execution op: unknown op code");
}
}
}
});
finishOperationBatch();
}
}