From 7aa993683579bc82be5a87174c90eab39872ee59 Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Tue, 4 Apr 2023 18:37:20 -0700 Subject: [PATCH] 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 --- .../Animated/__tests__/Animated-test.js | 2 - .../Animated/__tests__/Animated-web-test.js | 2 - .../Animated/nodes/AnimatedObject.js | 5 ++ .../Libraries/Animated/nodes/AnimatedProps.js | 30 +++++-- .../Libraries/Animated/nodes/AnimatedStyle.js | 88 +++++++------------ .../ReactNative/ReactNativeFeatureFlags.js | 5 ++ 6 files changed, 64 insertions(+), 68 deletions(-) diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js index eca451af1ba..96802ebf575 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js @@ -78,8 +78,6 @@ describe('Animated tests', () => { node.__attach(); - expect(anim.__getChildren().length).toBe(3); - anim.setValue(0.5); expect(callback).toBeCalled(); diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-web-test.js b/packages/react-native/Libraries/Animated/__tests__/Animated-web-test.js index 2fccade525c..3f2c71f96b7 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-web-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-web-test.js @@ -84,8 +84,6 @@ describe('Animated tests', () => { node.__attach(); - expect(anim.__getChildren().length).toBe(3); - anim.setValue(0.5); expect(callback).toBeCalled(); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js index 1cd82b8473d..96324c611c2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js @@ -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; diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index d6ebbb02e3b..0ce27f41d64 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -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; } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js index e851b6031de..eb48cd79fe2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js @@ -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 { - 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 { diff --git a/packages/react-native/Libraries/ReactNative/ReactNativeFeatureFlags.js b/packages/react-native/Libraries/ReactNative/ReactNativeFeatureFlags.js index 8881d287fe5..eb0e2fd29f5 100644 --- a/packages/react-native/Libraries/ReactNative/ReactNativeFeatureFlags.js +++ b/packages/react-native/Libraries/ReactNative/ReactNativeFeatureFlags.js @@ -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;