Animated: Optimize Traversals in Nodes (#46286)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/46286

Optimizes the runtime performance of `Animated` by using memoization to avoid repetitive traversals of `props` (and `style`) values.

Changelog:
[General][Changed] - Improved runtime performance of `Animated`

Reviewed By: javache

Differential Revision: D62037506

fbshipit-source-id: b0202f02c87466e1cef61b841de7e861a0ecae4e
This commit is contained in:
Tim Yung
2024-09-06 12:15:21 -07:00
committed by Facebook GitHub Bot
parent 9e35dffcf1
commit d1ebe02c19
9 changed files with 405 additions and 287 deletions
@@ -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);
});
});
@@ -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;
}
@@ -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<AnimatedNode> = [],
depth: number = 0,
): Array<AnimatedNode> {
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<AnimatedNode>;
_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<AnimatedNode>, 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);
}
@@ -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<string>, $ReadOnlyArray<AnimatedNode>, Object] {
const nodeKeys: Array<string> = [];
const nodes: Array<AnimatedNode> = [];
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<string>;
#nodes: $ReadOnlyArray<AnimatedNode>;
_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,
@@ -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<string>, $ReadOnlyArray<AnimatedNode>, Object] {
const nodeKeys: Array<string> = [];
const nodes: Array<AnimatedNode> = [];
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<string>;
#nodes: $ReadOnlyArray<AnimatedNode>;
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<Object> {
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__) {
@@ -27,22 +27,40 @@ type Transform<T = AnimatedNode> = {
};
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<AnimatedNode>;
_transforms: $ReadOnlyArray<Transform<>>;
constructor(transforms: $ReadOnlyArray<Transform<>>) {
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<any> = [];
const transformsConfig: Array<any> = [];
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<T>(
): $ReadOnlyArray<Transform<T>> {
return transforms.map(transform => {
const result: Transform<T> = {};
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
@@ -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<AnimatedNode>;
const {connectAnimatedNodes, disconnectAnimatedNodes} =
NativeAnimatedHelper.API;
constructor() {
super();
this._children = [];
}
export default class AnimatedWithChildren extends AnimatedNode {
_children: Array<AnimatedNode> = [];
__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());
@@ -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<AnimatedNode>, 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<Object>;
__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<AnimatedNode>;
constructor(): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__addChild(child: AnimatedNode): void;
__removeChild(child: AnimatedNode): void;
@@ -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', () => {