diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js index 76ddcf919ad..1f3d06389bd 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 519d03380ef..933af8fa24e 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"', - ); + let nativeTag = this.__nativeTag; + if (nativeTag == null) { + _assertNativeAnimatedModule?.(); - const nativeTag = - this.__nativeTag ?? NativeAnimatedHelper.generateNewNodeTag(); + // `__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 8388dd79b56..2f3ff765de6 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; - } - } - } - return false; -} - export default class AnimatedObject extends AnimatedWithChildren { - _value: any; + #nodes: $ReadOnlyArray; + _value: mixed; - constructor(value: any) { + /** + * 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); + } + + /** + * 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 042838aebfc..e0b642fd64f 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 1ba22a48559..bf08605e5f4 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 { - _inputStyle: any; - _style: Object; + #nodeKeys: $ReadOnlyArray; + #nodes: $ReadOnlyArray; - constructor(style: any) { + _inputStyle: any; + _style: {[string]: 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 43d4294812e..c0284d28c01 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; + + 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) { - this._transforms.forEach(transform => { - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - value.__makeNative(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 2c54d0263d8..21de142464d 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 5eb59f56510..9a3acc7b09a 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 @@ -907,7 +907,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; @@ -918,7 +918,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; @@ -938,10 +937,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; @@ -954,10 +953,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; @@ -976,8 +975,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; @@ -1139,7 +1138,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/__tests__/AnimatedNative-test.js b/packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js index 3c5ea6ee396..46a80a9dd5e 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', () => {