From 35e2a63b8d45a52445266bd8106600cb49081a59 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Tue, 17 May 2022 16:42:41 -0700 Subject: [PATCH] Batch Animated calls into one JSI call per frame Summary: We introduce a few optimizations: (1) Previous diff: We defer calling any NativeAnimatedModule methods by waiting 1ms before flushing the queue, and debouncing until no flush is requested. Practically, this just means that we'll call NativeAnimatedModule methods N times at once, at the end of a render loop, instead of N times smeared throughout the render loop. (2) Additionally, instead of calling N methods, we create multi-operation argument buffer and call a single NativeAnimatedModule API, which should essentially throttle NativeAnimatedModule API calls to once-ish per frame. On the native side, this also reduces a lot of overhead associated with scheduling work on the UI thread (we schedule 1 function to run on the UI thread and perform N operations, as opposed to scheduling N functions to run on the UI thread). TODO: - implement stubs for iOS - write gating code so this can be properly tested in VR and in fb4a Changelog: [Internal] Reviewed By: genkikondo Differential Revision: D36338606 fbshipit-source-id: 29ac949b53b874683128a76525586c22def3143b --- Libraries/Animated/NativeAnimatedHelper.js | 281 +++++++++++++----- Libraries/Animated/NativeAnimatedModule.js | 3 + .../Animated/NativeAnimatedTurboModule.js | 3 + .../RCTNativeAnimatedModule.mm | 5 + .../RCTNativeAnimatedTurboModule.mm | 5 + .../ReactNative/ReactNativeFeatureFlags.js | 7 + .../react/animated/NativeAnimatedModule.java | 227 ++++++++++++++ .../animated/NativeAnimatedNodesManager.java | 54 +++- 8 files changed, 510 insertions(+), 75 deletions(-) diff --git a/Libraries/Animated/NativeAnimatedHelper.js b/Libraries/Animated/NativeAnimatedHelper.js index 35c2e95adac..b8e88491d25 100644 --- a/Libraries/Animated/NativeAnimatedHelper.js +++ b/Libraries/Animated/NativeAnimatedHelper.js @@ -22,6 +22,8 @@ import type {AnimationConfig, EndCallback} from './animations/Animation'; import type {InterpolationConfigType} from './nodes/AnimatedInterpolation'; import ReactNativeFeatureFlags from '../ReactNative/ReactNativeFeatureFlags'; import invariant from 'invariant'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; +import type {EventSubscription} from '../vendor/emitter/EventEmitter'; // TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%. const NativeAnimatedModule = @@ -37,8 +39,20 @@ let nativeEventEmitter; let waitingForQueuedOperations = new Set(); let queueOperations = false; let queue: Array<() => void> = []; +// $FlowFixMe +let singleOpQueue: Array = []; +let useSingleOpBatching = + Platform.OS === 'android' && + !!NativeAnimatedModule?.queueAndExecuteBatchedOperations && + ReactNativeFeatureFlags.animatedShouldUseSingleOp(); let flushQueueTimeout = null; +let forceFlushQueueTimeout = null; + +const eventListenerGetValueCallbacks = {}; +const eventListenerAnimationFinishedCallbacks = {}; +let globalEventEmitterGetValueListener: ?EventSubscription = null; +let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null; /** * Simple wrappers around NativeAnimatedModule to provide flow and autocomplete support for @@ -50,9 +64,15 @@ const API = { saveValueCallback: (value: number) => void, ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => { - NativeAnimatedModule.getValue(tag, saveValueCallback); - }); + const args = [tag]; + if (useSingleOpBatching) { + if (saveValueCallback) { + eventListenerGetValueCallbacks[tag] = saveValueCallback; + } + } else { + args.push(saveValueCallback); + } + API.queueOperation(API.operationMap.getValue, args); }, setWaitingForIdentifier: function (id: string): void { waitingForQueuedOperations.add(id); @@ -76,39 +96,89 @@ const API = { invariant(NativeAnimatedModule, 'Native animated module is not available'); if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) { - clearTimeout(flushQueueTimeout); + const prevTimeout = flushQueueTimeout; + clearTimeout(prevTimeout); flushQueueTimeout = setTimeout(API.flushQueue, 1); + + // Force flushing within one frame, in case there are repeated re-renders + if (prevTimeout && !forceFlushQueueTimeout) { + forceFlushQueueTimeout = setTimeout(() => { + forceFlushQueueTimeout = null; + clearTimeout(flushQueueTimeout); + flushQueueTimeout = null; + API.flushQueue(); + }, 8); + } } else { API.flushQueue(); } }, flushQueue: function (): void { + flushQueueTimeout = null; + + // Early returns before calling any APIs + if (useSingleOpBatching && singleOpQueue.length === 0) { + return; + } + if (!useSingleOpBatching && queue.length === 0) { + return; + } + if (Platform.OS === 'android') { NativeAnimatedModule?.startOperationBatch?.(); } - for (let q = 0, l = queue.length; q < l; q++) { - queue[q](); + if (useSingleOpBatching) { + // Set up event listener for callbacks if it's not set up + if ( + !globalEventEmitterGetValueListener || + !globalEventEmitterAnimationFinishedListener + ) { + setupGlobalEventEmitterListeners(); + } + // Single op batching doesn't use callback functions, instead we + // use RCTDeviceEventEmitter. This reduces overhead of sending lots of + // JSI functions across to native code; but also, TM infrastructure currently + // does not support packing a function into native arrays. + NativeAnimatedModule?.queueAndExecuteBatchedOperations?.( + singleOpQueue.filter(x => typeof x !== 'function'), + ); + singleOpQueue.length = 0; + } else { + for (let q = 0, l = queue.length; q < l; q++) { + queue[q](); + } + queue.length = 0; } - queue.length = 0; if (Platform.OS === 'android') { NativeAnimatedModule?.finishOperationBatch?.(); } }, - queueOperation: (fn: () => void): void => { + queueOperation: ( + // $FlowFixMe + fn: (mode: 'immediate' | 'batch', ...args: Array) => number, + // $FlowFixMe + args: Array, + ): void => { + if (useSingleOpBatching) { + // Get the command ID from the queued function, and push that ID and any arguments needed to execute the operation + singleOpQueue.push(fn('batch'), ...args); + return; + } + // If queueing is explicitly on, *or* the queue has not yet // been flushed, use the queue. This is to prevent operations // from being executed out of order. if (queueOperations || queue.length !== 0) { - queue.push(fn); + queue.push(() => { + fn('immediate', ...args); + }); } else { - fn(); + fn('immediate', ...args); } }, createAnimatedNode: function (tag: number, config: AnimatedNodeConfig): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.createAnimatedNode(tag, config), - ); + API.queueOperation(API.operationMap.createAnimatedNode, [tag, config]); }, updateAnimatedNodeConfig: function ( tag: number, @@ -116,38 +186,40 @@ const API = { ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); if (typeof NativeAnimatedModule.updateAnimatedNodeConfig === 'function') { - API.queueOperation(() => - // $FlowIgnore[not-a-function] - checked above - NativeAnimatedModule.updateAnimatedNodeConfig(tag, config), - ); + API.queueOperation(API.operationMap.updateAnimatedNodeConfig, [ + tag, + config, + ]); } }, startListeningToAnimatedNodeValue: function (tag: number) { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.startListeningToAnimatedNodeValue(tag), - ); + API.queueOperation(API.operationMap.startListeningToAnimatedNodeValue, [ + tag, + ]); }, stopListeningToAnimatedNodeValue: function (tag: number) { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag), - ); + API.queueOperation(API.operationMap.stopListeningToAnimatedNodeValue, [ + tag, + ]); }, connectAnimatedNodes: function (parentTag: number, childTag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag), - ); + API.queueOperation(API.operationMap.connectAnimatedNodes, [ + parentTag, + childTag, + ]); }, disconnectAnimatedNodes: function ( parentTag: number, childTag: number, ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag), - ); + API.queueOperation(API.operationMap.disconnectAnimatedNodes, [ + parentTag, + childTag, + ]); }, startAnimatingNode: function ( animationId: number, @@ -156,70 +228,66 @@ const API = { endCallback: EndCallback, ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.startAnimatingNode( - animationId, - nodeTag, - config, - endCallback, - ), - ); + const args = [animationId, nodeTag, config]; + if (useSingleOpBatching) { + if (endCallback) { + eventListenerAnimationFinishedCallbacks[animationId] = endCallback; + } + } else { + args.push(endCallback); + } + API.queueOperation(API.operationMap.startAnimatingNode, args); }, stopAnimation: function (animationId: number) { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => NativeAnimatedModule.stopAnimation(animationId)); + API.queueOperation(API.operationMap.stopAnimation, [animationId]); }, setAnimatedNodeValue: function (nodeTag: number, value: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value), - ); + API.queueOperation(API.operationMap.setAnimatedNodeValue, [nodeTag, value]); }, setAnimatedNodeOffset: function (nodeTag: number, offset: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.setAnimatedNodeOffset(nodeTag, offset), - ); + API.queueOperation(API.operationMap.setAnimatedNodeOffset, [ + nodeTag, + offset, + ]); }, flattenAnimatedNodeOffset: function (nodeTag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.flattenAnimatedNodeOffset(nodeTag), - ); + API.queueOperation(API.operationMap.flattenAnimatedNodeOffset, [nodeTag]); }, extractAnimatedNodeOffset: function (nodeTag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.extractAnimatedNodeOffset(nodeTag), - ); + API.queueOperation(API.operationMap.extractAnimatedNodeOffset, [nodeTag]); }, connectAnimatedNodeToView: function (nodeTag: number, viewTag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag), - ); + API.queueOperation(API.operationMap.connectAnimatedNodeToView, [ + nodeTag, + viewTag, + ]); }, disconnectAnimatedNodeFromView: function ( nodeTag: number, viewTag: number, ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag), - ); + API.queueOperation(API.operationMap.disconnectAnimatedNodeFromView, [ + nodeTag, + viewTag, + ]); }, restoreDefaultValues: function (nodeTag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); // Backwards compat with older native runtimes, can be removed later. if (NativeAnimatedModule.restoreDefaultValues != null) { - API.queueOperation(() => - NativeAnimatedModule.restoreDefaultValues(nodeTag), - ); + API.queueOperation(API.operationMap.restoreDefaultValues, [nodeTag]); } }, dropAnimatedNode: function (tag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => NativeAnimatedModule.dropAnimatedNode(tag)); + API.queueOperation(API.operationMap.dropAnimatedNode, [tag]); }, addAnimatedEventToView: function ( viewTag: number, @@ -227,13 +295,11 @@ const API = { eventMapping: EventMapping, ) { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.addAnimatedEventToView( - viewTag, - eventName, - eventMapping, - ), - ); + API.queueOperation(API.operationMap.addAnimatedEventToView, [ + viewTag, + eventName, + eventMapping, + ]); }, removeAnimatedEventFromView( viewTag: number, @@ -241,16 +307,83 @@ const API = { animatedNodeTag: number, ) { invariant(NativeAnimatedModule, 'Native animated module is not available'); - API.queueOperation(() => - NativeAnimatedModule.removeAnimatedEventFromView( - viewTag, - eventName, - animatedNodeTag, - ), - ); + API.queueOperation(API.operationMap.removeAnimatedEventFromView, [ + viewTag, + eventName, + animatedNodeTag, + ]); }, + // $FlowFixMe + operationMap: (function () { + const apis = [ + 'createAnimatedNode', // 1 + 'updateAnimatedNodeConfig', // 2 + 'getValue', // 3 + 'startListeningToAnimatedNodeValue', // 4 + 'stopListeningToAnimatedNodeValue', // 5 + 'connectAnimatedNodes', // 6 + 'disconnectAnimatedNodes', // 7 + 'startAnimatingNode', // 8 + 'stopAnimation', // 9 + 'setAnimatedNodeValue', // 10 + 'setAnimatedNodeOffset', // 11 + 'flattenAnimatedNodeOffset', // 12 + 'extractAnimatedNodeOffset', // 13 + 'connectAnimatedNodeToView', // 14 + 'disconnectAnimatedNodeFromView', // 15 + 'restoreDefaultValues', // 16 + 'dropAnimatedNode', // 17 + 'addAnimatedEventToView', // 18 + 'removeAnimatedEventFromView', // 19 + 'addListener', // 20 + 'removeListener', // 21 + ]; + return apis.reduce((acc, functionName, i) => { + acc[functionName] = function ( + mode: 'batch' | 'immediate', + // $FlowFixMe + ...args: Array + ): number { + if (mode === 'immediate') { + // $FlowFixMe + NativeAnimatedModule?.[functionName](...args); + } + // These indices need to be kept in sync with the indices in native (see NativeAnimatedModule in Java, or the equivalent for any other native platform). + return i + 1; + }; + return acc; + }, {}); + })(), }; +function setupGlobalEventEmitterListeners() { + globalEventEmitterGetValueListener = RCTDeviceEventEmitter.addListener( + 'onNativeAnimatedModuleGetValue', + function (params) { + const {tag} = params; + const callback = eventListenerGetValueCallbacks[tag]; + if (!callback) { + return; + } + callback(params.value); + delete eventListenerGetValueCallbacks[tag]; + }, + ); + globalEventEmitterAnimationFinishedListener = + RCTDeviceEventEmitter.addListener( + 'onNativeAnimatedModuleAnimationFinished', + function (params) { + const {animationId} = params; + const callback = eventListenerAnimationFinishedCallbacks[animationId]; + if (!callback) { + return; + } + callback(params); + delete eventListenerAnimationFinishedCallbacks[animationId]; + }, + ); +} + /** * Styles allowed by the native animated implementation. * diff --git a/Libraries/Animated/NativeAnimatedModule.js b/Libraries/Animated/NativeAnimatedModule.js index 39ffb002f85..73f3bb76f90 100644 --- a/Libraries/Animated/NativeAnimatedModule.js +++ b/Libraries/Animated/NativeAnimatedModule.js @@ -64,6 +64,9 @@ export interface Spec extends TurboModule { // Events +addListener: (eventName: string) => void; +removeListeners: (count: number) => void; + + // All of the above in a batched mode + +queueAndExecuteBatchedOperations?: (operationsAndArgs: Array) => void; } export default (TurboModuleRegistry.get('NativeAnimatedModule'): ?Spec); diff --git a/Libraries/Animated/NativeAnimatedTurboModule.js b/Libraries/Animated/NativeAnimatedTurboModule.js index 7c0fbe8f8e2..3adac4237da 100644 --- a/Libraries/Animated/NativeAnimatedTurboModule.js +++ b/Libraries/Animated/NativeAnimatedTurboModule.js @@ -64,6 +64,9 @@ export interface Spec extends TurboModule { // Events +addListener: (eventName: string) => void; +removeListeners: (count: number) => void; + + // All of the above in a batched mode + +queueAndExecuteBatchedOperations?: (operationsAndArgs: Array) => void; } export default (TurboModuleRegistry.get( diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm index ddf060b2adc..50e8ca1ae8c 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm @@ -258,6 +258,11 @@ RCT_EXPORT_METHOD(getValue:(double)nodeTag saveValueCallback:(RCTResponseSenderB }]; } +RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations:(NSArray *)operationsAndArgs) { + // TODO: implement in the future if we want the same optimization here as on Android +} + + #pragma mark -- Batch handling - (void)addOperationBlock:(AnimatedOperation)operation diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm index 0e3be345049..9643b9f7d4e 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -269,6 +269,11 @@ RCT_EXPORT_METHOD(getValue:(double)nodeTag saveValueCallback:(RCTResponseSenderB }]; } +RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations:(NSArray *)operationsAndArgs) { + // TODO: implement in the future if we want the same optimization here as on Android +} + + #pragma mark -- Batch handling - (void)addOperationBlock:(AnimatedOperation)operation diff --git a/Libraries/ReactNative/ReactNativeFeatureFlags.js b/Libraries/ReactNative/ReactNativeFeatureFlags.js index 388eb54672b..7ad91c1fe1b 100644 --- a/Libraries/ReactNative/ReactNativeFeatureFlags.js +++ b/Libraries/ReactNative/ReactNativeFeatureFlags.js @@ -32,6 +32,12 @@ export type FeatureFlags = {| * Enables an experimental flush-queue debouncing in Animated.js. */ animatedShouldDebounceQueueFlush: () => boolean, + /** + * Enables an experimental mega-operation for Animated.js that replaces + * many calls to native with a single call into native, to reduce JSI/JNI + * traffic. + */ + animatedShouldUseSingleOp: () => boolean, |}; const ReactNativeFeatureFlags: FeatureFlags = { @@ -39,6 +45,7 @@ const ReactNativeFeatureFlags: FeatureFlags = { shouldEmitW3CPointerEvents: () => false, shouldPressibilityUseW3CPointerEventsForHover: () => false, animatedShouldDebounceQueueFlush: () => false, + animatedShouldUseSingleOp: () => false, }; module.exports = ReactNativeFeatureFlags; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index d84f69d8b10..5714c080e06 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -19,6 +19,7 @@ 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; @@ -92,6 +93,50 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec 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); @@ -973,4 +1018,186 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec 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. + * + *

The arguments operate as a byte buffer. All integer command IDs and any args are packed into + * opsAndArgs. + * + *

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) { + // 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. + final int opBufferSize = opsAndArgs.size(); + for (int i = 0; i < opBufferSize; ) { + BatchExecutionOpCodes command = BatchExecutionOpCodes.fromId(opsAndArgs.getInt(i++)); + switch (command) { + case OP_CODE_GET_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_START_LISTENING_TO_ANIMATED_NODE_VALUE: + 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 int value = 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.dropAnimatedNode(viewTag); + 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(); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index c4f2f8cdc20..ffb215fa616 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.common.UIManagerType; import com.facebook.react.uimanager.events.Event; @@ -302,6 +303,16 @@ import java.util.Queue; WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", false); animation.mEndCallback.invoke(endCallbackResponse); + } else if (mReactApplicationContext != null) { + // If no callback is passed in, this /may/ be an animation set up by the single-op + // instruction from JS, meaning that no jsi::functions are passed into native and + // we communicate via RCTDeviceEventEmitter instead of callbacks. + WritableMap params = Arguments.createMap(); + params.putInt("animationId", animation.mId); + params.putBoolean("finished", false); + mReactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onNativeAnimatedModuleAnimationFinished", params); } mActiveAnimations.removeAt(i); i--; @@ -323,6 +334,16 @@ import java.util.Queue; WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", false); animation.mEndCallback.invoke(endCallbackResponse); + } else if (mReactApplicationContext != null) { + // If no callback is passed in, this /may/ be an animation set up by the single-op + // instruction from JS, meaning that no jsi::functions are passed into native and + // we communicate via RCTDeviceEventEmitter instead of callbacks. + WritableMap params = Arguments.createMap(); + params.putInt("animationId", animation.mId); + params.putBoolean("finished", false); + mReactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onNativeAnimatedModuleAnimationFinished", params); } mActiveAnimations.removeAt(i); return; @@ -439,7 +460,25 @@ import java.util.Queue; throw new JSApplicationIllegalArgumentException( "getValue: Animated node with tag [" + tag + "] does not exist or is not a 'value' node"); } - callback.invoke(((ValueAnimatedNode) node).getValue()); + double value = ((ValueAnimatedNode) node).getValue(); + if (callback != null) { + callback.invoke(value); + return; + } + + // If there's no callback, that means that JS is using the single-operation mode, and not + // passing any callbacks into Java. + // See NativeAnimatedHelper.js for details. + // Instead, we use RCTDeviceEventEmitter to pass data back to JS and emulate callbacks. + if (mReactApplicationContext == null) { + return; + } + WritableMap params = Arguments.createMap(); + params.putInt("tag", tag); + params.putDouble("value", value); + mReactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onNativeAnimatedModuleGetValue", params); } @UiThread @@ -612,6 +651,19 @@ import java.util.Queue; WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", true); animation.mEndCallback.invoke(endCallbackResponse); + } else if (mReactApplicationContext != null) { + // If no callback is passed in, this /may/ be an animation set up by the single-op + // instruction from JS, meaning that no jsi::functions are passed into native and + // we communicate via RCTDeviceEventEmitter instead of callbacks. + WritableMap params = Arguments.createMap(); + params.putInt("animationId", animation.mId); + params.putBoolean("finished", true); + DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = + mReactApplicationContext.getJSModule( + DeviceEventManagerModule.RCTDeviceEventEmitter.class); + if (eventEmitter != null) { + eventEmitter.emit("onNativeAnimatedModuleAnimationFinished", params); + } } mActiveAnimations.removeAt(i); }