mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
9e35dffcf1
commit
d1ebe02c19
+43
-37
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+32
-23
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+74
-39
@@ -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,
|
||||
|
||||
+99
-58
@@ -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;
|
||||
|
||||
+26
-21
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user