UNPKG

react-native

Version:

A framework for building native apps using React

441 lines (387 loc) • 14.7 kB
/** * 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. * * @flow strict-local * @format */ import type {EventConfig} from '../../../Libraries/Animated/AnimatedEvent'; import type { AnimationConfig, EndCallback, } from '../../../Libraries/Animated/animations/Animation'; import type { AnimatedNodeConfig, EventMapping, } from '../../../Libraries/Animated/NativeAnimatedModule'; import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmitter'; import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule'; import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule'; import NativeEventEmitter from '../../../Libraries/EventEmitter/NativeEventEmitter'; import RCTDeviceEventEmitter from '../../../Libraries/EventEmitter/RCTDeviceEventEmitter'; import Platform from '../../../Libraries/Utilities/Platform'; import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlags'; import invariant from 'invariant'; import nullthrows from 'nullthrows'; // TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%. const NativeAnimatedModule: typeof NativeAnimatedTurboModule = NativeAnimatedNonTurboModule ?? NativeAnimatedTurboModule; let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ let __nativeAnimationIdCount = 1; /* used for started animations */ let nativeEventEmitter; let waitingForQueuedOperations = new Set<string>(); let queueOperations = false; let queue: Array<() => void> = []; let singleOpQueue: Array<mixed> = []; const isSingleOpBatching = Platform.OS === 'android' && NativeAnimatedModule?.queueAndExecuteBatchedOperations != null && ReactNativeFeatureFlags.animatedShouldUseSingleOp(); let flushQueueImmediate = null; const eventListenerGetValueCallbacks: { [number]: (value: number) => void, } = {}; const eventListenerAnimationFinishedCallbacks: { [number]: EndCallback, } = {}; let globalEventEmitterGetValueListener: ?EventSubscription = null; let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null; function createNativeOperations(): $NonMaybeType<typeof NativeAnimatedModule> { 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<typeof methodNames>]: (...$ReadOnlyArray<mixed>) => 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: (isSingleOpBatching ? (tag, saveValueCallback) => { if (saveValueCallback) { eventListenerGetValueCallbacks[tag] = saveValueCallback; } /* $FlowExpectedError[incompatible-call] - `saveValueCallback` is handled differently when `isSingleOpBatching` is enabled. */ NativeOperations.getValue(tag); } : (tag, saveValueCallback) => { NativeOperations.getValue(tag, saveValueCallback); }) as $NonMaybeType<typeof NativeAnimatedModule>['getValue'], setWaitingForIdentifier(id: string): void { waitingForQueuedOperations.add(id); queueOperations = true; if ( ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() && flushQueueImmediate ) { if (ReactNativeFeatureFlags.enableAnimatedClearImmediateFix()) { clearImmediate(flushQueueImmediate); } else { clearTimeout(flushQueueImmediate); } } }, unsetWaitingForIdentifier(id: string): void { waitingForQueuedOperations.delete(id); if (waitingForQueuedOperations.size === 0) { queueOperations = false; API.disableQueue(); } }, disableQueue(): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) { const prevImmediate = flushQueueImmediate; clearImmediate(prevImmediate); flushQueueImmediate = setImmediate(API.flushQueue); } else { API.flushQueue(); } }, flushQueue: (isSingleOpBatching ? (): void => { invariant( NativeAnimatedModule, 'Native animated module is not available', ); flushQueueImmediate = null; if (singleOpQueue.length === 0) { return; } // 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; } : (): void => { invariant( NativeAnimatedModule, 'Native animated module is not available', ); flushQueueImmediate = null; 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(tag: number, config: AnimatedNodeConfig): void { NativeOperations.updateAnimatedNodeConfig?.(tag, config); }, startListeningToAnimatedNodeValue(tag: number): void { NativeOperations.startListeningToAnimatedNodeValue(tag); }, stopListeningToAnimatedNodeValue(tag: number): void { NativeOperations.stopListeningToAnimatedNodeValue(tag); }, connectAnimatedNodes(parentTag: number, childTag: number): void { NativeOperations.connectAnimatedNodes(parentTag, childTag); }, disconnectAnimatedNodes(parentTag: number, childTag: number): void { NativeOperations.disconnectAnimatedNodes(parentTag, childTag); }, 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); } : (animationId, nodeTag, config, endCallback) => { NativeOperations.startAnimatingNode( animationId, nodeTag, config, endCallback, ); }) as $NonMaybeType<typeof NativeAnimatedModule>['startAnimatingNode'], stopAnimation(animationId: number) { NativeOperations.stopAnimation(animationId); }, setAnimatedNodeValue(nodeTag: number, value: number): void { NativeOperations.setAnimatedNodeValue(nodeTag, value); }, setAnimatedNodeOffset(nodeTag: number, offset: number): void { NativeOperations.setAnimatedNodeOffset(nodeTag, offset); }, flattenAnimatedNodeOffset(nodeTag: number): void { NativeOperations.flattenAnimatedNodeOffset(nodeTag); }, extractAnimatedNodeOffset(nodeTag: number): void { NativeOperations.extractAnimatedNodeOffset(nodeTag); }, connectAnimatedNodeToView(nodeTag: number, viewTag: number): void { NativeOperations.connectAnimatedNodeToView(nodeTag, viewTag); }, disconnectAnimatedNodeFromView(nodeTag: number, viewTag: number): void { NativeOperations.disconnectAnimatedNodeFromView(nodeTag, viewTag); }, restoreDefaultValues(nodeTag: number): void { NativeOperations.restoreDefaultValues?.(nodeTag); }, dropAnimatedNode(tag: number): void { NativeOperations.dropAnimatedNode(tag); }, addAnimatedEventToView( viewTag: number, eventName: string, eventMapping: EventMapping, ) { NativeOperations.addAnimatedEventToView(viewTag, eventName, eventMapping); }, removeAnimatedEventFromView( viewTag: number, eventName: string, animatedNodeTag: number, ) { NativeOperations.removeAnimatedEventFromView( viewTag, eventName, animatedNodeTag, ); }, }; function ensureGlobalEventEmitterListeners() { if ( globalEventEmitterGetValueListener && globalEventEmitterAnimationFinishedListener ) { return; } globalEventEmitterGetValueListener = RCTDeviceEventEmitter.addListener( 'onNativeAnimatedModuleGetValue', params => { const {tag} = params; const callback = eventListenerGetValueCallbacks[tag]; if (!callback) { return; } callback(params.value); delete eventListenerGetValueCallbacks[tag]; }, ); globalEventEmitterAnimationFinishedListener = RCTDeviceEventEmitter.addListener( 'onNativeAnimatedModuleAnimationFinished', params => { // TODO: remove Array.isArray once native changes have propagated const animations = Array.isArray(params) ? params : [params]; for (const animation of animations) { const {animationId} = animation; const callback = eventListenerAnimationFinishedCallbacks[animationId]; if (callback) { callback(animation); delete eventListenerAnimationFinishedCallbacks[animationId]; } } }, ); } function generateNewNodeTag(): number { return __nativeAnimatedNodeTagCount++; } function generateNewAnimationId(): number { return __nativeAnimationIdCount++; } function assertNativeAnimatedModule(): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); } let _warnedMissingNativeAnimated = false; function shouldUseNativeDriver( config: $ReadOnly<{...AnimationConfig, ...}> | EventConfig, ): boolean { if (config.useNativeDriver == null) { console.warn( 'Animated: `useNativeDriver` was not specified. This is a required ' + 'option and must be explicitly set to `true` or `false`', ); } if (config.useNativeDriver === true && !NativeAnimatedModule) { if (process.env.NODE_ENV !== 'test') { if (!_warnedMissingNativeAnimated) { console.warn( 'Animated: `useNativeDriver` is not supported because the native ' + 'animated module is missing. Falling back to JS-based animation. To ' + 'resolve this, add `RCTAnimation` module to this app, or remove ' + '`useNativeDriver`. ' + 'Make sure to run `bundle exec pod install` first. Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md', ); _warnedMissingNativeAnimated = true; } } return false; } return config.useNativeDriver || false; } function transformDataType(value: number | string): number | string { // Change the string type to number type so we can reuse the same logic in // iOS and Android platform if (typeof value !== 'string') { return value; } // Normalize degrees and radians to a number expressed in radians if (value.endsWith('deg')) { const degrees = parseFloat(value) || 0; return (degrees * Math.PI) / 180.0; } else if (value.endsWith('rad')) { return parseFloat(value) || 0; } else { return value; } } export default { API, generateNewNodeTag, generateNewAnimationId, assertNativeAnimatedModule, shouldUseNativeDriver, transformDataType, // $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppression // $FlowExpectedError[missing-type-arg] - unsafe getter lint suppression get nativeEventEmitter(): NativeEventEmitter { if (!nativeEventEmitter) { // $FlowFixMe[underconstrained-implicit-instantiation] nativeEventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior Platform.OS !== 'ios' ? null : NativeAnimatedModule, ); } return nativeEventEmitter; }, };