Introduce AnimatedObject JS node (#36688)

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

AnimatedObject is a more generic version of AnimatedTransform, able to handle animated values within arrays and objects. This is useful for props of native components that may need to be animated per field.

I considered flattening the node graph by removing AnimatedStyle and AnimatedTransform. However, this would add significant complexity in AnimatedProps because prop and style values depend on being submitted together on an animation tick (such as transform) using native driver; also, we'll have to special case style anyway.

Changelog:
[Internal][Added] - Introduce AnimatedObject JS node for handling array and object prop values

Reviewed By: rshest

Differential Revision: D44279594

fbshipit-source-id: 9504d841dc9196e51d09a0247601de4d4f991a49
This commit is contained in:
Genki Kondo
2023-03-31 10:16:36 -07:00
committed by Facebook GitHub Bot
parent 64033637e2
commit d0fcd43f1a
2 changed files with 203 additions and 0 deletions
@@ -0,0 +1,66 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import Animated from '../Animated';
import AnimatedObject, {hasAnimatedNode} from '../nodes/AnimatedObject';
describe('AnimatedObject', () => {
beforeEach(() => {
jest.resetModules();
});
it('should get the proper value', () => {
const anim = new Animated.Value(0);
const translateAnim = anim.interpolate({
inputRange: [0, 1],
outputRange: [100, 200],
});
const node = new AnimatedObject([
{
translate: [translateAnim, translateAnim],
},
{
translateX: translateAnim,
},
{scale: anim},
]);
expect(node.__getValue()).toEqual([
{translate: [100, 100]},
{translateX: 100},
{scale: 0},
]);
});
describe('hasAnimatedNode', () => {
it('should detect any animated nodes', () => {
expect(hasAnimatedNode(10)).toBe(false);
const anim = new Animated.Value(0);
expect(hasAnimatedNode(anim)).toBe(true);
const event = Animated.event([{}], {useNativeDriver: true});
expect(hasAnimatedNode(event)).toBe(false);
expect(hasAnimatedNode([10, 10])).toBe(false);
expect(hasAnimatedNode([10, anim])).toBe(true);
expect(hasAnimatedNode({a: 10, b: 10})).toBe(false);
expect(hasAnimatedNode({a: 10, b: anim})).toBe(true);
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);
});
});
});
@@ -0,0 +1,137 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
* @oncall react_native
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
const MAX_DEPTH = 5;
function isPlainObject(value: any): boolean {
return (
value !== null &&
typeof value === 'object' &&
Object.getPrototypeOf(value).isPrototypeOf(Object)
);
}
// Recurse through values, executing fn for any AnimatedNodes
function visit(value: any, fn: any => void, depth: number = 0): void {
if (depth >= MAX_DEPTH) {
return;
}
if (value instanceof AnimatedNode) {
fn(value);
} else if (Array.isArray(value)) {
value.forEach(element => {
visit(element, fn, depth + 1);
});
} else if (isPlainObject(value)) {
Object.values(value).forEach(element => {
visit(element, fn, depth + 1);
});
}
}
// Returns a copy of value with a transformation fn applied to any AnimatedNodes
function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any {
if (depth >= MAX_DEPTH) {
return value;
}
if (value instanceof AnimatedNode) {
return fn(value);
} else if (Array.isArray(value)) {
return value.map(element => mapAnimatedNodes(element, fn, depth + 1));
} else if (isPlainObject(value)) {
const result: {[string]: any} = {};
for (const key in value) {
result[key] = mapAnimatedNodes(value[key], fn, depth + 1);
}
return result;
} else {
return value;
}
}
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;
constructor(value: any) {
super();
this._value = value;
}
__getValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getValue();
});
}
__getAnimatedValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getAnimatedValue();
});
}
__attach(): void {
super.__attach();
visit(this._value, node => {
node.__addChild(this);
});
}
__detach(): void {
visit(this._value, node => {
node.__removeChild(this);
});
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig): void {
throw new Error(
'This JS animated node type cannot be used as native animated node',
);
}
__getNativeConfig(): any {
throw new Error(
'This JS animated node type cannot be used as native animated node',
);
}
}