diff --git a/packages/react-native/src/private/animated/NativeAnimatedHelper.js b/packages/react-native/src/private/animated/NativeAnimatedHelper.js index 913c84bf7bcc20..fac57567b1e747 100644 --- a/packages/react-native/src/private/animated/NativeAnimatedHelper.js +++ b/packages/react-native/src/private/animated/NativeAnimatedHelper.js @@ -16,7 +16,6 @@ import type { } from '../../../Libraries/Animated/animations/Animation'; import type { AnimatedNodeConfig, - AnimatingNodeConfig, EventMapping, } from '../../../Libraries/Animated/NativeAnimatedModule'; @@ -27,9 +26,10 @@ import Platform from '../../../Libraries/Utilities/Platform'; import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule'; import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule'; import invariant from 'invariant'; +import nullthrows from 'nullthrows'; // TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%. -const NativeAnimatedModule = +const NativeAnimatedModule: typeof NativeAnimatedTurboModule = NativeAnimatedNonTurboModule ?? NativeAnimatedTurboModule; let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ @@ -40,12 +40,11 @@ let nativeEventEmitter; let waitingForQueuedOperations = new Set(); let queueOperations = false; let queue: Array<() => void> = []; -// $FlowFixMe -let singleOpQueue: Array = []; +let singleOpQueue: Array = []; -const useSingleOpBatching = +const isSingleOpBatching = Platform.OS === 'android' && - !!NativeAnimatedModule?.queueAndExecuteBatchedOperations && + NativeAnimatedModule?.queueAndExecuteBatchedOperations != null && ReactNativeFeatureFlags.animatedShouldUseSingleOp(); let flushQueueTimeout = null; @@ -58,61 +57,87 @@ const eventListenerAnimationFinishedCallbacks: { let globalEventEmitterGetValueListener: ?EventSubscription = null; let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null; -const nativeOps: ?typeof NativeAnimatedModule = useSingleOpBatching - ? ((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<{[string]: number}>((acc, functionName, i) => { - // 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). - // $FlowFixMe[prop-missing] - acc[functionName] = i + 1; - return acc; - }, {}); - })(): $FlowFixMe) - : NativeAnimatedModule; +function createNativeOperations(): $NonMaybeType { + const methodNames = [ + '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 + ]; + const nativeOperations: { + [$Values]: (...$ReadOnlyArray) => void, + } = {}; + if (isSingleOpBatching) { + for (let ii = 0, length = methodNames.length; ii < length; ii++) { + const methodName = methodNames[ii]; + const operationID = ii + 1; + nativeOperations[methodName] = (...args) => { + // `singleOpQueue` is a flat array of operation IDs and arguments, which + // is possible because # arguments is fixed for each operation. For more + // details, see `NativeAnimatedModule.queueAndExecuteBatchedOperations`. + singleOpQueue.push(operationID, ...args); + }; + } + } else { + for (let ii = 0, length = methodNames.length; ii < length; ii++) { + const methodName = methodNames[ii]; + nativeOperations[methodName] = (...args) => { + const method = nullthrows(NativeAnimatedModule)[methodName]; + // 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) { + // $FlowExpectedError[incompatible-call] - Dynamism. + queue.push(() => method(...args)); + } else { + // $FlowExpectedError[incompatible-call] - Dynamism. + method(...args); + } + }; + } + } + // $FlowExpectedError[incompatible-return] - Dynamism. + return nativeOperations; +} + +const NativeOperations = createNativeOperations(); /** * Wrappers around NativeAnimatedModule to provide flow and autocomplete support for * the native module methods, and automatic queue management on Android */ const API = { - getValue: function ( - tag: number, - saveValueCallback: (value: number) => void, - ): void { - invariant(nativeOps, 'Native animated module is not available'); - if (useSingleOpBatching) { - if (saveValueCallback) { - eventListenerGetValueCallbacks[tag] = saveValueCallback; + getValue: (isSingleOpBatching + ? (tag, saveValueCallback) => { + if (saveValueCallback) { + eventListenerGetValueCallbacks[tag] = saveValueCallback; + } + /* $FlowExpectedError[incompatible-call] - `saveValueCallback` is handled + differently when `isSingleOpBatching` is enabled. */ + NativeOperations.getValue(tag); } - // $FlowFixMe - API.queueOperation(nativeOps.getValue, tag); - } else { - API.queueOperation(nativeOps.getValue, tag, saveValueCallback); - } - }, - setWaitingForIdentifier: function (id: string): void { + : (tag, saveValueCallback) => { + NativeOperations.getValue(tag, saveValueCallback); + }) as $NonMaybeType['getValue'], + + setWaitingForIdentifier(id: string): void { waitingForQueuedOperations.add(id); queueOperations = true; if ( @@ -122,7 +147,8 @@ const API = { clearTimeout(flushQueueTimeout); } }, - unsetWaitingForIdentifier: function (id: string): void { + + unsetWaitingForIdentifier(id: string): void { waitingForQueuedOperations.delete(id); if (waitingForQueuedOperations.size === 0) { @@ -130,8 +156,9 @@ const API = { API.disableQueue(); } }, - disableQueue: function (): void { - invariant(nativeOps, 'Native animated module is not available'); + + disableQueue(): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) { const prevTimeout = flushQueueTimeout; @@ -141,196 +168,148 @@ const API = { API.flushQueue(); } }, - flushQueue: function (): void { - // TODO: (T136971132) - invariant( - NativeAnimatedModule || process.env.NODE_ENV === 'test', - 'Native animated module is not available', - ); - flushQueueTimeout = null; - // Early returns before calling any APIs - if (useSingleOpBatching && singleOpQueue.length === 0) { - return; - } - if (!useSingleOpBatching && queue.length === 0) { - return; - } + flushQueue: (isSingleOpBatching + ? (): void => { + // TODO: (T136971132) + invariant( + NativeAnimatedModule || process.env.NODE_ENV === 'test', + 'Native animated module is not available', + ); + flushQueueTimeout = null; - 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); - singleOpQueue.length = 0; - } else { - Platform.OS === 'android' && - NativeAnimatedModule?.startOperationBatch?.(); + if (singleOpQueue.length === 0) { + return; + } - for (let q = 0, l = queue.length; q < l; q++) { - queue[q](); + // Set up event listener for callbacks if it's not set up + ensureGlobalEventEmitterListeners(); + + // 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); + singleOpQueue.length = 0; } - queue.length = 0; - Platform.OS === 'android' && - NativeAnimatedModule?.finishOperationBatch?.(); - } - }, - queueOperation: , Fn: (...Args) => void>( - fn: Fn, - ...args: Args - ): void => { - if (useSingleOpBatching) { - // Get the command ID from the queued function, and push that ID and any arguments needed to execute the operation - // $FlowFixMe: surprise, fn is actually a number - singleOpQueue.push(fn, ...args); - return; - } + : (): void => { + // TODO: (T136971132) + invariant( + NativeAnimatedModule || process.env.NODE_ENV === 'test', + 'Native animated module is not available', + ); + flushQueueTimeout = null; - // 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(...args)); - } else { - fn(...args); - } - }, - createAnimatedNode: function (tag: number, config: AnimatedNodeConfig): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.createAnimatedNode, tag, config); + if (queue.length === 0) { + return; + } + + if (Platform.OS === 'android') { + NativeAnimatedModule?.startOperationBatch?.(); + } + + for (let q = 0, l = queue.length; q < l; q++) { + queue[q](); + } + queue.length = 0; + + if (Platform.OS === 'android') { + NativeAnimatedModule?.finishOperationBatch?.(); + } + }) as () => void, + + createAnimatedNode(tag: number, config: AnimatedNodeConfig): void { + NativeOperations.createAnimatedNode(tag, config); }, - updateAnimatedNodeConfig: function ( - tag: number, - config: AnimatedNodeConfig, - ): void { - invariant(nativeOps, 'Native animated module is not available'); - if (nativeOps.updateAnimatedNodeConfig) { - API.queueOperation(nativeOps.updateAnimatedNodeConfig, tag, config); - } + + updateAnimatedNodeConfig(tag: number, config: AnimatedNodeConfig): void { + NativeOperations.updateAnimatedNodeConfig?.(tag, config); }, - startListeningToAnimatedNodeValue: function (tag: number) { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.startListeningToAnimatedNodeValue, tag); + + startListeningToAnimatedNodeValue(tag: number): void { + NativeOperations.startListeningToAnimatedNodeValue(tag); }, - stopListeningToAnimatedNodeValue: function (tag: number) { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.stopListeningToAnimatedNodeValue, tag); + + stopListeningToAnimatedNodeValue(tag: number): void { + NativeOperations.stopListeningToAnimatedNodeValue(tag); }, - connectAnimatedNodes: function (parentTag: number, childTag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.connectAnimatedNodes, parentTag, childTag); + + connectAnimatedNodes(parentTag: number, childTag: number): void { + NativeOperations.connectAnimatedNodes(parentTag, childTag); }, - disconnectAnimatedNodes: function ( - parentTag: number, - childTag: number, - ): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.disconnectAnimatedNodes, parentTag, childTag); + + disconnectAnimatedNodes(parentTag: number, childTag: number): void { + NativeOperations.disconnectAnimatedNodes(parentTag, childTag); }, - startAnimatingNode: function ( - animationId: number, - nodeTag: number, - config: AnimatingNodeConfig, - endCallback: EndCallback, - ): void { - invariant(nativeOps, 'Native animated module is not available'); - if (useSingleOpBatching) { - if (endCallback) { - eventListenerAnimationFinishedCallbacks[animationId] = endCallback; + + startAnimatingNode: (isSingleOpBatching + ? (animationId, nodeTag, config, endCallback) => { + if (endCallback) { + eventListenerAnimationFinishedCallbacks[animationId] = endCallback; + } + /* $FlowExpectedError[incompatible-call] - `endCallback` is handled + differently when `isSingleOpBatching` is enabled. */ + NativeOperations.startAnimatingNode(animationId, nodeTag, config); } - // $FlowFixMe - API.queueOperation( - // $FlowFixMe[incompatible-call] - nativeOps.startAnimatingNode, - animationId, - nodeTag, - config, - ); - } else { - API.queueOperation( - nativeOps.startAnimatingNode, - animationId, - nodeTag, - config, - endCallback, - ); - } - }, - stopAnimation: function (animationId: number) { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.stopAnimation, animationId); + : (animationId, nodeTag, config, endCallback) => { + NativeOperations.startAnimatingNode( + animationId, + nodeTag, + config, + endCallback, + ); + }) as $NonMaybeType['startAnimatingNode'], + + stopAnimation(animationId: number) { + NativeOperations.stopAnimation(animationId); }, - setAnimatedNodeValue: function (nodeTag: number, value: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.setAnimatedNodeValue, nodeTag, value); + + setAnimatedNodeValue(nodeTag: number, value: number): void { + NativeOperations.setAnimatedNodeValue(nodeTag, value); }, - setAnimatedNodeOffset: function (nodeTag: number, offset: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.setAnimatedNodeOffset, nodeTag, offset); + + setAnimatedNodeOffset(nodeTag: number, offset: number): void { + NativeOperations.setAnimatedNodeOffset(nodeTag, offset); }, - flattenAnimatedNodeOffset: function (nodeTag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.flattenAnimatedNodeOffset, nodeTag); + + flattenAnimatedNodeOffset(nodeTag: number): void { + NativeOperations.flattenAnimatedNodeOffset(nodeTag); }, - extractAnimatedNodeOffset: function (nodeTag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.extractAnimatedNodeOffset, nodeTag); + + extractAnimatedNodeOffset(nodeTag: number): void { + NativeOperations.extractAnimatedNodeOffset(nodeTag); }, - connectAnimatedNodeToView: function (nodeTag: number, viewTag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.connectAnimatedNodeToView, nodeTag, viewTag); + + connectAnimatedNodeToView(nodeTag: number, viewTag: number): void { + NativeOperations.connectAnimatedNodeToView(nodeTag, viewTag); }, - disconnectAnimatedNodeFromView: function ( - nodeTag: number, - viewTag: number, - ): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation( - nativeOps.disconnectAnimatedNodeFromView, - nodeTag, - viewTag, - ); + + disconnectAnimatedNodeFromView(nodeTag: number, viewTag: number): void { + NativeOperations.disconnectAnimatedNodeFromView(nodeTag, viewTag); }, - restoreDefaultValues: function (nodeTag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - // Backwards compat with older native runtimes, can be removed later. - if (nativeOps.restoreDefaultValues != null) { - API.queueOperation(nativeOps.restoreDefaultValues, nodeTag); - } + + restoreDefaultValues(nodeTag: number): void { + NativeOperations.restoreDefaultValues?.(nodeTag); }, - dropAnimatedNode: function (tag: number): void { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation(nativeOps.dropAnimatedNode, tag); + + dropAnimatedNode(tag: number): void { + NativeOperations.dropAnimatedNode(tag); }, - addAnimatedEventToView: function ( + + addAnimatedEventToView( viewTag: number, eventName: string, eventMapping: EventMapping, ) { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation( - nativeOps.addAnimatedEventToView, - viewTag, - eventName, - eventMapping, - ); + NativeOperations.addAnimatedEventToView(viewTag, eventName, eventMapping); }, + removeAnimatedEventFromView( viewTag: number, eventName: string, animatedNodeTag: number, ) { - invariant(nativeOps, 'Native animated module is not available'); - API.queueOperation( - nativeOps.removeAnimatedEventFromView, + NativeOperations.removeAnimatedEventFromView( viewTag, eventName, animatedNodeTag, @@ -338,7 +317,13 @@ const API = { }, }; -function setupGlobalEventEmitterListeners() { +function ensureGlobalEventEmitterListeners() { + if ( + globalEventEmitterGetValueListener && + globalEventEmitterAnimationFinishedListener + ) { + return; + } globalEventEmitterGetValueListener = RCTDeviceEventEmitter.addListener( 'onNativeAnimatedModuleGetValue', params => {