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

Differential Revision: D44279594

fbshipit-source-id: 3ba28441d207071a3d4314238fcbfc70913cb321
  • Loading branch information
genkikondo authored and facebook-github-bot committed Mar 28, 2023
1 parent a7f7f8a commit c9810c3
Show file tree
Hide file tree
Showing 2 changed files with 186 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);
});
});
});
120 changes: 120 additions & 0 deletions packages/react-native/Libraries/Animated/nodes/AnimatedObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* 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 {AnimatedEvent} from '../AnimatedEvent';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';

// Recurse through values, executing fn for any AnimatedNodes
function visit(value: any, fn: any => void): void {
if (value instanceof AnimatedNode) {
fn(value);
} else if (Array.isArray(value)) {
value.forEach(element => {
visit(element, fn);
});
} else if (typeof value === 'object') {
Object.values(value).forEach(element => {
visit(element, fn);
});
}
}

// Returns a copy of value with a transformation fn applied to any AnimatedNodes
function mapAnimatedNodes(value: any, fn: any => any): any {
if (value instanceof AnimatedNode) {
return fn(value);
} else if (Array.isArray(value)) {
return value.map(element => mapAnimatedNodes(element, fn));
} else if (typeof value === 'object') {
const result: {[string]: any} = {};
for (const key in value) {
result[key] = mapAnimatedNodes(value[key], fn);
}
return result;
} else {
return value;
}
}

export function hasAnimatedNode(value: any): boolean {
if (value instanceof AnimatedNode) {
return true;
} else if (Array.isArray(value)) {
for (const element of value) {
if (hasAnimatedNode(element)) {
return true;
}
}
} else if (
value !== null &&
typeof value === 'object' &&
Object.getPrototypeOf(value).isPrototypeOf(Object)
) {
for (const key in value) {
if (hasAnimatedNode(value[key])) {
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 c9810c3

Please sign in to comment.