Skip to content

Commit

Permalink
Introduce AnimatedObject JS node (#36688)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
genkikondo authored and facebook-github-bot committed Mar 31, 2023
1 parent 6403363 commit d0fcd43
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
137 changes: 137 additions & 0 deletions packages/react-native/Libraries/Animated/nodes/AnimatedObject.js
Original file line number Diff line number Diff line change
@@ -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',
);
}
}

0 comments on commit d0fcd43

Please sign in to comment.