Modify AnimatedProps and AnimatedStyle to use AnimatedObject (#36798)

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

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.

This diff hooks up AnimatedObject to AnimatedProps and AnimatedStyle for values that are arrays or objects.

Changelog:
[Internal][Added] - Modify AnimatedProps and AnimatedStyle to use AnimatedObject

Reviewed By: rshest

Differential Revision: D44637985

fbshipit-source-id: c70b9d40e40d0782c2c1a332f1f22358fe0abe64
This commit is contained in:
Genki Kondo
2023-04-04 18:37:20 -07:00
committed by Facebook GitHub Bot
parent 816598975c
commit 7aa9936835
6 changed files with 64 additions and 68 deletions
@@ -78,8 +78,6 @@ describe('Animated tests', () => {
node.__attach();
expect(anim.__getChildren().length).toBe(3);
anim.setValue(0.5);
expect(callback).toBeCalled();
@@ -84,8 +84,6 @@ describe('Animated tests', () => {
node.__attach();
expect(anim.__getChildren().length).toBe(3);
anim.setValue(0.5);
expect(callback).toBeCalled();
@@ -15,6 +15,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
import * as React from 'react';
const MAX_DEPTH = 5;
@@ -80,6 +81,10 @@ export function hasAnimatedNode(value: any, depth: number = 0): boolean {
}
}
} else if (isPlainObject(value)) {
// Don't consider React elements
if (React.isValidElement(value)) {
return false;
}
for (const key in value) {
if (hasAnimatedNode(value[key], depth + 1)) {
return true;
@@ -12,13 +12,35 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import ReactNativeFeatureFlags from '../../ReactNative/ReactNativeFeatureFlags';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
import {AnimatedEvent} from '../AnimatedEvent';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
import AnimatedObject, {hasAnimatedNode} from './AnimatedObject';
import AnimatedStyle from './AnimatedStyle';
import invariant from 'invariant';
function createAnimatedProps(inputProps: Object): Object {
const props: Object = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
props[key] = new AnimatedStyle(value);
} else if (value instanceof AnimatedNode) {
props[key] = value;
} else if (
ReactNativeFeatureFlags.isAnimatedObjectEnabled &&
hasAnimatedNode(value)
) {
props[key] = new AnimatedObject(value);
} else {
props[key] = value;
}
}
return props;
}
export default class AnimatedProps extends AnimatedNode {
_props: Object;
_animatedView: any;
@@ -26,13 +48,7 @@ export default class AnimatedProps extends AnimatedNode {
constructor(props: Object, callback: () => void) {
super();
if (props.style) {
props = {
...props,
style: new AnimatedStyle(props.style),
};
}
this._props = props;
this._props = createAnimatedProps(props);
this._callback = callback;
}
@@ -12,14 +12,19 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import ReactNativeFeatureFlags from '../../ReactNative/ReactNativeFeatureFlags';
import flattenStyle from '../../StyleSheet/flattenStyle';
import Platform from '../../Utilities/Platform';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
import AnimatedObject, {hasAnimatedNode} from './AnimatedObject';
import AnimatedTransform from './AnimatedTransform';
import AnimatedWithChildren from './AnimatedWithChildren';
function createAnimatedStyle(inputStyle: any): Object {
function createAnimatedStyle(
inputStyle: any,
keepUnanimatedValues: boolean,
): Object {
// $FlowFixMe[underconstrained-implicit-instantiation]
const style = flattenStyle(inputStyle);
const animatedStyles: any = {};
@@ -29,82 +34,51 @@ function createAnimatedStyle(inputStyle: any): Object {
animatedStyles[key] = new AnimatedTransform(value);
} else if (value instanceof AnimatedNode) {
animatedStyles[key] = value;
} else if (value && !Array.isArray(value) && typeof value === 'object') {
animatedStyles[key] = createAnimatedStyle(value);
} else if (
ReactNativeFeatureFlags.isAnimatedObjectEnabled &&
hasAnimatedNode(value)
) {
animatedStyles[key] = new AnimatedObject(value);
} else if (keepUnanimatedValues) {
animatedStyles[key] = value;
}
}
return animatedStyles;
}
function createStyleWithAnimatedTransform(inputStyle: any): Object {
// $FlowFixMe[underconstrained-implicit-instantiation]
let style = flattenStyle(inputStyle) || ({}: {[string]: any});
if (style.transform) {
style = {
...style,
transform: new AnimatedTransform(style.transform),
};
}
return style;
}
export default class AnimatedStyle extends AnimatedWithChildren {
_inputStyle: any;
_style: Object;
constructor(style: any) {
super();
if (Platform.OS === 'web') {
this._inputStyle = style;
this._style = createAnimatedStyle(style);
} else {
this._style = createStyleWithAnimatedTransform(style);
}
}
// Recursively get values for nested styles (like iOS's shadowOffset)
_walkStyleAndGetValues(style: any): {[string]: any | {...}} {
const updatedStyle: {[string]: any | {...}} = {};
for (const key in style) {
const value = style[key];
if (value instanceof AnimatedNode) {
updatedStyle[key] = value.__getValue();
} else if (value && !Array.isArray(value) && typeof value === 'object') {
// Support animating nested values (for example: shadowOffset.height)
updatedStyle[key] = this._walkStyleAndGetValues(value);
} else {
updatedStyle[key] = value;
}
}
return updatedStyle;
this._inputStyle = style;
this._style = createAnimatedStyle(style, Platform.OS !== 'web');
}
__getValue(): Object | Array<Object> {
if (Platform.OS === 'web') {
return [this._inputStyle, this._walkStyleAndGetValues(this._style)];
}
return this._walkStyleAndGetValues(this._style);
}
// Recursively get animated values for nested styles (like iOS's shadowOffset)
_walkStyleAndGetAnimatedValues(style: any): {[string]: any | {...}} {
const updatedStyle: {[string]: any | {...}} = {};
for (const key in style) {
const value = style[key];
const result: {[string]: any} = {};
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
updatedStyle[key] = value.__getAnimatedValue();
} else if (value && !Array.isArray(value) && typeof value === 'object') {
// Support animating nested values (for example: shadowOffset.height)
updatedStyle[key] = this._walkStyleAndGetAnimatedValues(value);
result[key] = value.__getValue();
} else {
result[key] = value;
}
}
return updatedStyle;
return Platform.OS === 'web' ? [this._inputStyle, result] : result;
}
__getAnimatedValue(): Object {
return this._walkStyleAndGetAnimatedValues(this._style);
const result: {[string]: any} = {};
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
result[key] = value.__getAnimatedValue();
}
}
return result;
}
__attach(): void {
@@ -45,6 +45,10 @@ export type FeatureFlags = {|
* Enables access to the host tree in Fabric using DOM-compatible APIs.
*/
enableAccessToHostTreeInFabric: () => boolean,
/**
* Enables animating object and array prop values.
*/
isAnimatedObjectEnabled: () => boolean,
|};
const ReactNativeFeatureFlags: FeatureFlags = {
@@ -55,6 +59,7 @@ const ReactNativeFeatureFlags: FeatureFlags = {
animatedShouldUseSingleOp: () => false,
isGlobalWebPerformanceLoggerEnabled: () => false,
enableAccessToHostTreeInFabric: () => false,
isAnimatedObjectEnabled: () => false,
};
module.exports = ReactNativeFeatureFlags;