-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce AnimatedObject JS node (#36688)
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
1 parent
6403363
commit d0fcd43
Showing
2 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
137
packages/react-native/Libraries/Animated/nodes/AnimatedObject.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
); | ||
} | ||
} |