diff --git a/packages/metro-config/src/index.flow.js b/packages/metro-config/src/index.flow.js index 17cc19fc6823e4..4912c5c0ebccfd 100644 --- a/packages/metro-config/src/index.flow.js +++ b/packages/metro-config/src/index.flow.js @@ -87,6 +87,7 @@ export function getDefaultConfig(projectRoot: string): ConfigT { babelTransformerPath: require.resolve( '@react-native/metro-babel-transformer', ), + hermesParser: true, getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js index 76ddcf919ade66..1f3d06389bd2a9 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js @@ -4,17 +4,21 @@ * 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 * @oncall react_native */ -import Animated from '../Animated'; -import AnimatedObject, {hasAnimatedNode} from '../nodes/AnimatedObject'; +import nullthrows from 'nullthrows'; describe('AnimatedObject', () => { + let Animated; + let AnimatedObject; + beforeEach(() => { jest.resetModules(); + + Animated = require('../Animated').default; + AnimatedObject = require('../nodes/AnimatedObject').default; }); it('should get the proper value', () => { @@ -24,15 +28,17 @@ describe('AnimatedObject', () => { outputRange: [100, 200], }); - const node = new AnimatedObject([ - { - translate: [translateAnim, translateAnim], - }, - { - translateX: translateAnim, - }, - {scale: anim}, - ]); + const node = nullthrows( + AnimatedObject.from([ + { + translate: [translateAnim, translateAnim], + }, + { + translateX: translateAnim, + }, + {scale: anim}, + ]), + ); expect(node.__getValue()).toEqual([ {translate: [100, 100]}, @@ -48,15 +54,17 @@ describe('AnimatedObject', () => { outputRange: [100, 200], }); - const node = new AnimatedObject([ - { - translate: [translateAnim, translateAnim], - }, - { - translateX: translateAnim, - }, - {scale: anim}, - ]); + const node = nullthrows( + AnimatedObject.from([ + { + translate: [translateAnim, translateAnim], + }, + { + translateX: translateAnim, + }, + {scale: anim}, + ]), + ); node.__makeNative(); @@ -65,26 +73,24 @@ describe('AnimatedObject', () => { expect(translateAnim.__isNative).toBe(true); }); - describe('hasAnimatedNode', () => { - it('should detect any animated nodes', () => { - expect(hasAnimatedNode(10)).toBe(false); + it('detects animated nodes', () => { + expect(AnimatedObject.from(10)).toBe(null); - const anim = new Animated.Value(0); - expect(hasAnimatedNode(anim)).toBe(true); + const anim = new Animated.Value(0); + expect(AnimatedObject.from(anim)).not.toBe(null); - const event = Animated.event([{}], {useNativeDriver: true}); - expect(hasAnimatedNode(event)).toBe(false); + const event = Animated.event([{}], {useNativeDriver: true}); + expect(AnimatedObject.from(event)).toBe(null); - expect(hasAnimatedNode([10, 10])).toBe(false); - expect(hasAnimatedNode([10, anim])).toBe(true); + expect(AnimatedObject.from([10, 10])).toBe(null); + expect(AnimatedObject.from([10, anim])).not.toBe(null); - expect(hasAnimatedNode({a: 10, b: 10})).toBe(false); - expect(hasAnimatedNode({a: 10, b: anim})).toBe(true); + expect(AnimatedObject.from({a: 10, b: 10})).toBe(null); + expect(AnimatedObject.from({a: 10, b: anim})).not.toBe(null); - expect(hasAnimatedNode({a: 10, b: {ba: 10, bb: 10}})).toBe(false); - expect(hasAnimatedNode({a: 10, b: {ba: 10, bb: anim}})).toBe(true); - expect(hasAnimatedNode({a: 10, b: [10, 10]})).toBe(false); - expect(hasAnimatedNode({a: 10, b: [10, anim]})).toBe(true); - }); + expect(AnimatedObject.from({a: 10, b: {ba: 10, bb: 10}})).toBe(null); + expect(AnimatedObject.from({a: 10, b: {ba: 10, bb: anim}})).not.toBe(null); + expect(AnimatedObject.from({a: 10, b: [10, 10]})).toBe(null); + expect(AnimatedObject.from({a: 10, b: [10, anim]})).not.toBe(null); }); }); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js index 519d03380ef7b0..933af8fa24e450 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js @@ -10,23 +10,31 @@ 'use strict'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import invariant from 'invariant'; -const NativeAnimatedAPI = NativeAnimatedHelper.API; +const {startListeningToAnimatedNodeValue, stopListeningToAnimatedNodeValue} = + NativeAnimatedHelper.API; type ValueListenerCallback = (state: {value: number, ...}) => mixed; let _uniqueId = 1; +let _assertNativeAnimatedModule: ?() => void = () => { + NativeAnimatedHelper.assertNativeAnimatedModule(); + // We only have to assert that the module exists once. After we've asserted + // this, clear out the function so we know to skip it in the future. + _assertNativeAnimatedModule = null; +}; // Note(vjeux): this would be better as an interface but flow doesn't // support them yet export default class AnimatedNode { - _listeners: {[key: string]: ValueListenerCallback, ...}; - _platformConfig: ?PlatformConfig; - __nativeAnimatedValueListener: ?any; + _listeners: {[key: string]: ValueListenerCallback, ...} = {}; + _platformConfig: ?PlatformConfig = undefined; + __nativeAnimatedValueListener: ?EventSubscription = null; __attach(): void {} __detach(): void { this.removeAllListeners(); @@ -46,13 +54,9 @@ export default class AnimatedNode { } /* Methods and props used by native Animated impl */ - __isNative: boolean; - __nativeTag: ?number; - __shouldUpdateListenersForNewNativeTag: boolean; - - constructor() { - this._listeners = {}; - } + __isNative: boolean = false; + __nativeTag: ?number = undefined; + __shouldUpdateListenersForNewNativeTag: boolean = false; __makeNative(platformConfig: ?PlatformConfig): void { if (!this.__isNative) { @@ -123,7 +127,7 @@ export default class AnimatedNode { this._stopListeningForNativeValueUpdates(); } - NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); + startListeningToAnimatedNodeValue(this.__getNativeTag()); this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener( 'onAnimatedValueUpdate', @@ -141,8 +145,9 @@ export default class AnimatedNode { } __callListeners(value: number): void { + const event = {value}; for (const key in this._listeners) { - this._listeners[key]({value}); + this._listeners[key](event); } } @@ -153,21 +158,24 @@ export default class AnimatedNode { this.__nativeAnimatedValueListener.remove(); this.__nativeAnimatedValueListener = null; - NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag()); + stopListeningToAnimatedNodeValue(this.__getNativeTag()); } __getNativeTag(): number { - NativeAnimatedHelper.assertNativeAnimatedModule(); - invariant( - this.__isNative, - 'Attempt to get native tag from node not marked as "native"', - ); - - const nativeTag = - this.__nativeTag ?? NativeAnimatedHelper.generateNewNodeTag(); + let nativeTag = this.__nativeTag; + if (nativeTag == null) { + _assertNativeAnimatedModule?.(); + + // `__isNative` is initialized as false and only ever set to true. So we + // only need to check it once here when initializing `__nativeTag`. + invariant( + this.__isNative, + 'Attempt to get native tag from node not marked as "native"', + ); - if (this.__nativeTag == null) { + nativeTag = NativeAnimatedHelper.generateNewNodeTag(); this.__nativeTag = nativeTag; + const config = this.__getNativeConfig(); if (this._platformConfig) { config.platformConfig = this._platformConfig; @@ -175,9 +183,9 @@ export default class AnimatedNode { NativeAnimatedHelper.API.createAnimatedNode(nativeTag, config); this.__shouldUpdateListenersForNewNativeTag = true; } - return nativeTag; } + __getNativeConfig(): Object { throw new Error( 'This JS animated node type cannot be used as native animated node', @@ -191,6 +199,7 @@ export default class AnimatedNode { __getPlatformConfig(): ?PlatformConfig { return this._platformConfig; } + __setPlatformConfig(platformConfig: ?PlatformConfig) { this._platformConfig = platformConfig; } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js index 8388dd79b56995..2f3ff765de60ff 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js @@ -19,7 +19,9 @@ import * as React from 'react'; const MAX_DEPTH = 5; -function isPlainObject(value: any): boolean { +/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype + and ReactElement checks preserve the type refinement of `value`. */ +function isPlainObject(value: mixed): value is $ReadOnly<{[string]: mixed}> { return ( value !== null && typeof value === 'object' && @@ -28,23 +30,29 @@ function isPlainObject(value: any): boolean { ); } -// Recurse through values, executing fn for any AnimatedNodes -function visit(value: any, fn: any => void, depth: number = 0): void { +function flatAnimatedNodes( + value: mixed, + nodes: Array = [], + depth: number = 0, +): Array { if (depth >= MAX_DEPTH) { - return; + return nodes; } - if (value instanceof AnimatedNode) { - fn(value); + nodes.push(value); } else if (Array.isArray(value)) { - value.forEach(element => { - visit(element, fn, depth + 1); - }); + for (let ii = 0, length = value.length; ii < length; ii++) { + const element = value[ii]; + flatAnimatedNodes(element, nodes, depth + 1); + } } else if (isPlainObject(value)) { - Object.values(value).forEach(element => { - visit(element, fn, depth + 1); - }); + const keys = Object.keys(value); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; + flatAnimatedNodes(value[key], nodes, depth + 1); + } } + return nodes; } // Returns a copy of value with a transformation fn applied to any AnimatedNodes @@ -59,7 +67,9 @@ function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any { return value.map(element => mapAnimatedNodes(element, fn, depth + 1)); } else if (isPlainObject(value)) { const result: {[string]: any} = {}; - for (const key in value) { + const keys = Object.keys(value); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; result[key] = mapAnimatedNodes(value[key], fn, depth + 1); } return result; @@ -68,34 +78,28 @@ function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any { } } -export function hasAnimatedNode(value: any, depth: number = 0): boolean { - if (depth >= MAX_DEPTH) { - return false; - } - - if (value instanceof AnimatedNode) { - return true; - } else if (Array.isArray(value)) { - for (const element of value) { - if (hasAnimatedNode(element, depth + 1)) { - return true; - } - } - } else if (isPlainObject(value)) { - for (const key in value) { - if (hasAnimatedNode(value[key], depth + 1)) { - return true; - } +export default class AnimatedObject extends AnimatedWithChildren { + #nodes: $ReadOnlyArray; + _value: mixed; + + /** + * Creates an `AnimatedObject` if `value` contains `AnimatedNode` instances. + * Otherwise, returns `null`. + */ + static from(value: mixed): ?AnimatedObject { + const nodes = flatAnimatedNodes(value); + if (nodes.length === 0) { + return null; } + return new AnimatedObject(nodes, value); } - return false; -} -export default class AnimatedObject extends AnimatedWithChildren { - _value: any; - - constructor(value: any) { + /** + * Should only be called by `AnimatedObject.from`. + */ + constructor(nodes: $ReadOnlyArray, value: mixed) { super(); + this.#nodes = nodes; this._value = value; } @@ -112,23 +116,28 @@ export default class AnimatedObject extends AnimatedWithChildren { } __attach(): void { - super.__attach(); - visit(this._value, node => { + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; node.__addChild(this); - }); + } } __detach(): void { - visit(this._value, node => { + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; node.__removeChild(this); - }); + } super.__detach(); } __makeNative(platformConfig: ?PlatformConfig): void { - visit(this._value, value => { - value.__makeNative(platformConfig); - }); + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__makeNative(platformConfig); + } super.__makeNative(platformConfig); } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index 042838aebfc0b0..e0b642fd64feff 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -16,42 +16,72 @@ import {findNodeHandle} from '../../ReactNative/RendererProxy'; import {AnimatedEvent} from '../AnimatedEvent'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; -import AnimatedObject, {hasAnimatedNode} from './AnimatedObject'; +import AnimatedObject from './AnimatedObject'; import AnimatedStyle from './AnimatedStyle'; import invariant from 'invariant'; -function createAnimatedProps(inputProps: Object): Object { +function createAnimatedProps( + inputProps: Object, +): [$ReadOnlyArray, $ReadOnlyArray, Object] { + const nodeKeys: Array = []; + const nodes: Array = []; const props: Object = {}; - for (const key in inputProps) { + + const keys = Object.keys(inputProps); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; const value = inputProps[key]; + if (key === 'style') { - props[key] = new AnimatedStyle(value); + const node = new AnimatedStyle(value); + nodeKeys.push(key); + nodes.push(node); + props[key] = node; } else if (value instanceof AnimatedNode) { - props[key] = value; - } else if (hasAnimatedNode(value)) { - props[key] = new AnimatedObject(value); + const node = value; + nodeKeys.push(key); + nodes.push(node); + props[key] = node; } else { - props[key] = value; + const node = AnimatedObject.from(value); + if (node == null) { + props[key] = value; + } else { + nodeKeys.push(key); + nodes.push(node); + props[key] = node; + } } } - return props; + + return [nodeKeys, nodes, props]; } export default class AnimatedProps extends AnimatedNode { + #nodeKeys: $ReadOnlyArray; + #nodes: $ReadOnlyArray; + + _animatedView: any = null; _props: Object; - _animatedView: any; _callback: () => void; - constructor(props: Object, callback: () => void) { + constructor(inputProps: Object, callback: () => void) { super(); - this._props = createAnimatedProps(props); + const [nodeKeys, nodes, props] = createAnimatedProps(inputProps); + this.#nodeKeys = nodeKeys; + this.#nodes = nodes; + this._props = props; this._callback = callback; } __getValue(): Object { const props: {[string]: any | ((...args: any) => void)} = {}; - for (const key in this._props) { + + const keys = Object.keys(this._props); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; const value = this._props[key]; + if (value instanceof AnimatedNode) { props[key] = value.__getValue(); } else if (value instanceof AnimatedEvent) { @@ -66,21 +96,23 @@ export default class AnimatedProps extends AnimatedNode { __getAnimatedValue(): Object { const props: {[string]: any} = {}; - for (const key in this._props) { - const value = this._props[key]; - if (value instanceof AnimatedNode) { - props[key] = value.__getAnimatedValue(); - } + + const nodeKeys = this.#nodeKeys; + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const key = nodeKeys[ii]; + const node = nodes[ii]; + props[key] = node.__getAnimatedValue(); } + return props; } __attach(): void { - for (const key in this._props) { - const value = this._props[key]; - if (value instanceof AnimatedNode) { - value.__addChild(this); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__addChild(this); } } @@ -90,12 +122,12 @@ export default class AnimatedProps extends AnimatedNode { } this._animatedView = null; - for (const key in this._props) { - const value = this._props[key]; - if (value instanceof AnimatedNode) { - value.__removeChild(this); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__removeChild(this); } + super.__detach(); } @@ -104,11 +136,10 @@ export default class AnimatedProps extends AnimatedNode { } __makeNative(platformConfig: ?PlatformConfig): void { - for (const key in this._props) { - const value = this._props[key]; - if (value instanceof AnimatedNode) { - value.__makeNative(platformConfig); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__makeNative(platformConfig); } if (!this.__isNative) { @@ -172,14 +203,18 @@ export default class AnimatedProps extends AnimatedNode { } __getNativeConfig(): Object { + const platformConfig = this.__getPlatformConfig(); const propsConfig: {[string]: number} = {}; - for (const propKey in this._props) { - const value = this._props[propKey]; - if (value instanceof AnimatedNode) { - value.__makeNative(this.__getPlatformConfig()); - propsConfig[propKey] = value.__getNativeTag(); - } + + const nodeKeys = this.#nodeKeys; + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const key = nodeKeys[ii]; + const node = nodes[ii]; + node.__makeNative(platformConfig); + propsConfig[key] = node.__getNativeTag(); } + return { type: 'props', props: propsConfig, diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js index 1ba22a4855959f..bf08605e5f4706 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js @@ -17,109 +17,150 @@ import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/Reac import flattenStyle from '../../StyleSheet/flattenStyle'; import Platform from '../../Utilities/Platform'; import AnimatedNode from './AnimatedNode'; -import AnimatedObject, {hasAnimatedNode} from './AnimatedObject'; +import AnimatedObject from './AnimatedObject'; import AnimatedTransform from './AnimatedTransform'; import AnimatedWithChildren from './AnimatedWithChildren'; function createAnimatedStyle( - inputStyle: any, + inputStyle: {[string]: mixed}, keepUnanimatedValues: boolean, -): Object { - // $FlowFixMe[underconstrained-implicit-instantiation] - const style = flattenStyle(inputStyle); - const animatedStyles: any = {}; - for (const key in style) { - const value = style[key]; +): [$ReadOnlyArray, $ReadOnlyArray, Object] { + const nodeKeys: Array = []; + const nodes: Array = []; + const style: {[string]: any} = {}; + + const keys = Object.keys(inputStyle); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; + const value = inputStyle[key]; + if (value != null && key === 'transform') { - animatedStyles[key] = - ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() - ? new AnimatedObject(value) - : new AnimatedTransform(value); + const node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() + ? AnimatedObject.from(value) + : // $FlowFixMe[incompatible-call] - `value` is mixed. + new AnimatedTransform(value); + if (node == null) { + if (keepUnanimatedValues) { + style[key] = value; + } + } else { + nodeKeys.push(key); + nodes.push(node); + style[key] = node; + } } else if (value instanceof AnimatedNode) { - animatedStyles[key] = value; - } else if (hasAnimatedNode(value)) { - animatedStyles[key] = new AnimatedObject(value); - } else if (keepUnanimatedValues) { - animatedStyles[key] = value; + const node = value; + nodeKeys.push(key); + nodes.push(node); + style[key] = value; + } else { + const node = AnimatedObject.from(value); + if (node == null) { + if (keepUnanimatedValues) { + style[key] = value; + } + } else { + nodeKeys.push(key); + nodes.push(node); + style[key] = node; + } } } - return animatedStyles; + + return [nodeKeys, nodes, style]; } export default class AnimatedStyle extends AnimatedWithChildren { + #nodeKeys: $ReadOnlyArray; + #nodes: $ReadOnlyArray; + _inputStyle: any; - _style: Object; + _style: {[string]: any}; - constructor(style: any) { + constructor(inputStyle: any) { super(); - this._inputStyle = style; - this._style = createAnimatedStyle(style, Platform.OS !== 'web'); + this._inputStyle = inputStyle; + const [nodeKeys, nodes, style] = createAnimatedStyle( + // NOTE: This null check should not be necessary, but the types are not + // strong nor enforced as of this writing. This check should be hoisted + // to instantiation sites. + flattenStyle(inputStyle) ?? {}, + Platform.OS !== 'web', + ); + this.#nodeKeys = nodeKeys; + this.#nodes = nodes; + this._style = style; } __getValue(): Object | Array { - const result: {[string]: any} = {}; - for (const key in this._style) { + const style: {[string]: any} = {}; + + const keys = Object.keys(this._style); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; const value = this._style[key]; + if (value instanceof AnimatedNode) { - result[key] = value.__getValue(); + style[key] = value.__getValue(); } else { - result[key] = value; + style[key] = value; } } - return Platform.OS === 'web' ? [this._inputStyle, result] : result; + return Platform.OS === 'web' ? [this._inputStyle, style] : style; } __getAnimatedValue(): Object { - const result: {[string]: any} = {}; - for (const key in this._style) { - const value = this._style[key]; - if (value instanceof AnimatedNode) { - result[key] = value.__getAnimatedValue(); - } + const style: {[string]: any} = {}; + + const nodeKeys = this.#nodeKeys; + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const key = nodeKeys[ii]; + const node = nodes[ii]; + style[key] = node.__getAnimatedValue(); } - return result; + + return style; } __attach(): void { - for (const key in this._style) { - const value = this._style[key]; - if (value instanceof AnimatedNode) { - value.__addChild(this); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__addChild(this); } } __detach(): void { - for (const key in this._style) { - const value = this._style[key]; - if (value instanceof AnimatedNode) { - value.__removeChild(this); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__removeChild(this); } super.__detach(); } __makeNative(platformConfig: ?PlatformConfig) { - for (const key in this._style) { - const value = this._style[key]; - if (value instanceof AnimatedNode) { - value.__makeNative(platformConfig); - } + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__makeNative(platformConfig); } super.__makeNative(platformConfig); } __getNativeConfig(): Object { + const platformConfig = this.__getPlatformConfig(); const styleConfig: {[string]: ?number} = {}; - for (const styleKey in this._style) { - if (this._style[styleKey] instanceof AnimatedNode) { - const style = this._style[styleKey]; - style.__makeNative(this.__getPlatformConfig()); - styleConfig[styleKey] = style.__getNativeTag(); - } - // Non-animated styles are set using `setNativeProps`, no need - // to pass those as a part of the node config + + const nodeKeys = this.#nodeKeys; + const nodes = this.#nodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const key = nodeKeys[ii]; + const node = nodes[ii]; + node.__makeNative(platformConfig); + styleConfig[key] = node.__getNativeTag(); } if (__DEV__) { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js index 43d4294812e8ee..c0284d28c01bc2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js @@ -27,22 +27,40 @@ type Transform = { }; export default class AnimatedTransform extends AnimatedWithChildren { + // NOTE: For potentially historical reasons, some operations only operate on + // the first level of AnimatedNode instances. This optimizes that bevavior. + #shallowNodes: $ReadOnlyArray; + _transforms: $ReadOnlyArray>; constructor(transforms: $ReadOnlyArray>) { super(); this._transforms = transforms; - } - __makeNative(platformConfig: ?PlatformConfig) { - this._transforms.forEach(transform => { - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - value.__makeNative(platformConfig); + const shallowNodes = []; + // NOTE: This check should not be necessary, but the types are not enforced + // as of this writing. This check should be hoisted to instantiation sites. + if (Array.isArray(transforms)) { + for (let ii = 0, length = transforms.length; ii < length; ii++) { + const transform = transforms[ii]; + // There should be exactly one property in `transform`. + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + shallowNodes.push(value); + } } } - }); + } + this.#shallowNodes = shallowNodes; + } + + __makeNative(platformConfig: ?PlatformConfig) { + const nodes = this.#shallowNodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__makeNative(platformConfig); + } super.__makeNative(platformConfig); } @@ -59,42 +77,39 @@ export default class AnimatedTransform extends AnimatedWithChildren { } __attach(): void { - this._transforms.forEach(transform => { - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - value.__addChild(this); - } - } - }); + const nodes = this.#shallowNodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__addChild(this); + } } __detach(): void { - this._transforms.forEach(transform => { - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - value.__removeChild(this); - } - } - }); + const nodes = this.#shallowNodes; + for (let ii = 0, length = nodes.length; ii < length; ii++) { + const node = nodes[ii]; + node.__removeChild(this); + } super.__detach(); } __getNativeConfig(): any { - const transConfigs: Array = []; + const transformsConfig: Array = []; - this._transforms.forEach(transform => { + const transforms = this._transforms; + for (let ii = 0, length = transforms.length; ii < length; ii++) { + const transform = transforms[ii]; + // There should be exactly one property in `transform`. for (const key in transform) { const value = transform[key]; if (value instanceof AnimatedNode) { - transConfigs.push({ + transformsConfig.push({ type: 'animated', property: key, nodeTag: value.__getNativeTag(), }); } else { - transConfigs.push({ + transformsConfig.push({ type: 'static', property: key, /* $FlowFixMe[incompatible-call] - `value` can be an array or an @@ -104,14 +119,14 @@ export default class AnimatedTransform extends AnimatedWithChildren { }); } } - }); + } if (__DEV__) { - validateTransform(transConfigs); + validateTransform(transformsConfig); } return { type: 'transform', - transforms: transConfigs, + transforms: transformsConfig, }; } } @@ -122,6 +137,7 @@ function mapTransforms( ): $ReadOnlyArray> { return transforms.map(transform => { const result: Transform = {}; + // There should be exactly one property in `transform`. for (const key in transform) { const value = transform[key]; if (value instanceof AnimatedNode) { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js b/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js index 2c54d0263d8820..21de142464d9f6 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js @@ -15,23 +15,26 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; -export default class AnimatedWithChildren extends AnimatedNode { - _children: Array; +const {connectAnimatedNodes, disconnectAnimatedNodes} = + NativeAnimatedHelper.API; - constructor() { - super(); - this._children = []; - } +export default class AnimatedWithChildren extends AnimatedNode { + _children: Array = []; __makeNative(platformConfig: ?PlatformConfig) { if (!this.__isNative) { this.__isNative = true; - for (const child of this._children) { - child.__makeNative(platformConfig); - NativeAnimatedHelper.API.connectAnimatedNodes( - this.__getNativeTag(), - child.__getNativeTag(), - ); + + const children = this._children; + let length = children.length; + if (length > 0) { + const nativeTag = this.__getNativeTag(); + + for (let ii = 0; ii < length; ii++) { + const child = children[ii]; + child.__makeNative(platformConfig); + connectAnimatedNodes(nativeTag, child.__getNativeTag()); + } } } super.__makeNative(platformConfig); @@ -45,10 +48,7 @@ export default class AnimatedWithChildren extends AnimatedNode { if (this.__isNative) { // Only accept "native" animated nodes as children child.__makeNative(this.__getPlatformConfig()); - NativeAnimatedHelper.API.connectAnimatedNodes( - this.__getNativeTag(), - child.__getNativeTag(), - ); + connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); } } @@ -59,10 +59,7 @@ export default class AnimatedWithChildren extends AnimatedNode { return; } if (this.__isNative && child.__isNative) { - NativeAnimatedHelper.API.disconnectAnimatedNodes( - this.__getNativeTag(), - child.__getNativeTag(), - ); + disconnectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); } this._children.splice(index, 1); if (this._children.length === 0) { @@ -77,7 +74,9 @@ export default class AnimatedWithChildren extends AnimatedNode { __callListeners(value: number): void { super.__callListeners(value); if (!this.__isNative) { - for (const child of this._children) { + const children = this._children; + for (let ii = 0, length = children.length; ii < length; ii++) { + const child = children[ii]; // $FlowFixMe[method-unbinding] added when improving typing for this parameters if (child.__getValue) { child.__callListeners(child.__getValue()); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 1c51017ab8a4ac..353e9fe89c379d 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -906,7 +906,7 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A declare export default class AnimatedNode { _listeners: { [key: string]: ValueListenerCallback, ... }; _platformConfig: ?PlatformConfig; - __nativeAnimatedValueListener: ?any; + __nativeAnimatedValueListener: ?EventSubscription; __attach(): void; __detach(): void; __getValue(): any; @@ -917,7 +917,6 @@ declare export default class AnimatedNode { __isNative: boolean; __nativeTag: ?number; __shouldUpdateListenersForNewNativeTag: boolean; - constructor(): void; __makeNative(platformConfig: ?PlatformConfig): void; addListener(callback: (value: any) => mixed): string; removeListener(id: string): void; @@ -937,10 +936,10 @@ declare export default class AnimatedNode { `; exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedObject.js 1`] = ` -"declare export function hasAnimatedNode(value: any, depth: number): boolean; -declare export default class AnimatedObject extends AnimatedWithChildren { - _value: any; - constructor(value: any): void; +"declare export default class AnimatedObject extends AnimatedWithChildren { + _value: mixed; + static from(value: mixed): ?AnimatedObject; + constructor(nodes: $ReadOnlyArray, value: mixed): void; __getValue(): any; __getAnimatedValue(): any; __attach(): void; @@ -953,10 +952,10 @@ declare export default class AnimatedObject extends AnimatedWithChildren { exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedProps.js 1`] = ` "declare export default class AnimatedProps extends AnimatedNode { - _props: Object; _animatedView: any; + _props: Object; _callback: () => void; - constructor(props: Object, callback: () => void): void; + constructor(inputProps: Object, callback: () => void): void; __getValue(): Object; __getAnimatedValue(): Object; __attach(): void; @@ -975,8 +974,8 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedStyle.js 1`] = ` "declare export default class AnimatedStyle extends AnimatedWithChildren { _inputStyle: any; - _style: Object; - constructor(style: any): void; + _style: { [string]: any }; + constructor(inputStyle: any): void; __getValue(): Object | Array; __getAnimatedValue(): Object; __attach(): void; @@ -1138,7 +1137,6 @@ declare export default class AnimatedValueXY extends AnimatedWithChildren { exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedWithChildren.js 1`] = ` "declare export default class AnimatedWithChildren extends AnimatedNode { _children: Array; - constructor(): void; __makeNative(platformConfig: ?PlatformConfig): void; __addChild(child: AnimatedNode): void; __removeChild(child: AnimatedNode): void; 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 => { diff --git a/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js b/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js index 3c5ea6ee39687e..46a80a9dd5eda7 100644 --- a/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js +++ b/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js @@ -8,36 +8,38 @@ * @oncall react_native */ -jest - .clearAllMocks() - .mock('../../../../Libraries/BatchedBridge/NativeModules', () => ({ - NativeAnimatedModule: {}, - PlatformConstants: { - getConstants() { - return {}; - }, - }, - })) - .mock('../../specs/modules/NativeAnimatedModule') - .mock('../../../../Libraries/EventEmitter/NativeEventEmitter') - // findNodeHandle is imported from RendererProxy so mock that whole module. - .setMock('../../../../Libraries/ReactNative/RendererProxy', { - findNodeHandle: () => 1, - }); - import {format} from 'node:util'; import * as React from 'react'; import {createRef} from 'react'; const {create, unmount, update} = require('../../../../jest/renderer'); -const Animated = require('../../../../Libraries/Animated/Animated').default; -const NativeAnimatedHelper = require('../NativeAnimatedHelper').default; describe('Native Animated', () => { - const NativeAnimatedModule = - require('../../specs/modules/NativeAnimatedModule').default; + let Animated; + let NativeAnimatedHelper; + let NativeAnimatedModule; beforeEach(() => { + jest.resetModules(); + jest + .clearAllMocks() + .mock('../../../../Libraries/BatchedBridge/NativeModules', () => ({ + NativeAnimatedModule: {}, + PlatformConstants: { + getConstants() { + return {}; + }, + }, + })) + .mock('../../specs/modules/NativeAnimatedModule') + .mock('../../../../Libraries/EventEmitter/NativeEventEmitter') + // findNodeHandle is imported from RendererProxy so mock that whole module. + .setMock('../../../../Libraries/ReactNative/RendererProxy', { + findNodeHandle: () => 1, + }); + + NativeAnimatedModule = + require('../../specs/modules/NativeAnimatedModule').default; Object.assign(NativeAnimatedModule, { getValue: jest.fn(), addAnimatedEventToView: jest.fn(), @@ -58,6 +60,9 @@ describe('Native Animated', () => { stopAnimation: jest.fn(), stopListeningToAnimatedNodeValue: jest.fn(), }); + + Animated = require('../../../../Libraries/Animated/Animated').default; + NativeAnimatedHelper = require('../NativeAnimatedHelper').default; }); describe('Animated Value', () => {