From a248456d18a9bdf3d4c5d8665a1b60be79ff29b1 Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Wed, 5 Apr 2023 07:27:38 -0700 Subject: [PATCH] Introduce ObjectAnimatedNode (#36742) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36742 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 adds the native (Android) counterpart to AnimatedObject node in JS. The node handles array and map value types. Changelog: [Internal][Added] - Introduce ObjectAnimatedNode Java-side node for handling array and object prop values Reviewed By: mdvacca Differential Revision: D44466563 fbshipit-source-id: bd026cbd921ec51ae17eab08417708f3272c0418 --- .../Animated/__tests__/AnimatedObject-test.js | 24 +++ .../Animated/nodes/AnimatedObject.js | 16 +- .../animated/NativeAnimatedNodesManager.java | 2 + .../react/animated/ObjectAnimatedNode.java | 159 ++++++++++++++++++ .../react/animated/PropsAnimatedNode.java | 2 + .../react/animated/StyleAnimatedNode.java | 2 + 6 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ObjectAnimatedNode.java diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js index 8447d8453afb87..76ddcf919ade66 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js @@ -41,6 +41,30 @@ describe('AnimatedObject', () => { ]); }); + it('should make all AnimatedNodes native', () => { + 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}, + ]); + + node.__makeNative(); + + expect(node.__isNative).toBe(true); + expect(anim.__isNative).toBe(true); + expect(translateAnim.__isNative).toBe(true); + }); + describe('hasAnimatedNode', () => { it('should detect any animated nodes', () => { expect(hasAnimatedNode(10)).toBe(false); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js index 96324c611c200c..0db05822131bc3 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js @@ -129,14 +129,18 @@ export default class AnimatedObject extends AnimatedWithChildren { } __makeNative(platformConfig: ?PlatformConfig): void { - throw new Error( - 'This JS animated node type cannot be used as native animated node', - ); + visit(this._value, value => { + value.__makeNative(platformConfig); + }); + super.__makeNative(platformConfig); } __getNativeConfig(): any { - throw new Error( - 'This JS animated node type cannot be used as native animated node', - ); + return { + type: 'object', + value: mapAnimatedNodes(this._value, node => { + return {nodeTag: node.__getNativeTag()}; + }), + }; } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 7ccfe4c2fece28..4ca3aa3a180c3d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -141,6 +141,8 @@ public void createAnimatedNode(int tag, ReadableMap config) { node = new TransformAnimatedNode(config, this); } else if ("tracking".equals(type)) { node = new TrackingAnimatedNode(config, this); + } else if ("object".equals(type)) { + node = new ObjectAnimatedNode(config, this); } else { throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ObjectAnimatedNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ObjectAnimatedNode.java new file mode 100644 index 00000000000000..b00863f82f2ffa --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ObjectAnimatedNode.java @@ -0,0 +1,159 @@ +/* + * 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. + */ + +package com.facebook.react.animated; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; + +/** + * Native counterpart of object animated node (see AnimatedObject class in + * AnimatedImplementation.js) + */ +/* package */ class ObjectAnimatedNode extends AnimatedNode { + + private static final String VALUE_KEY = "value"; + private static final String NODE_TAG_KEY = "nodeTag"; + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final JavaOnlyMap mConfig; + + ObjectAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mConfig = JavaOnlyMap.deepClone(config); + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + } + + public void collectViewUpdates(String propKey, JavaOnlyMap propsMap) { + ReadableType valueType = mConfig.getType(VALUE_KEY); + if (valueType == ReadableType.Map) { + propsMap.putMap(propKey, collectViewUpdatesHelper(mConfig.getMap(VALUE_KEY))); + } else if (valueType == ReadableType.Array) { + propsMap.putArray(propKey, collectViewUpdatesHelper(mConfig.getArray(VALUE_KEY))); + } else { + throw new IllegalArgumentException("Invalid value type for ObjectAnimatedNode"); + } + } + + private @Nullable JavaOnlyArray collectViewUpdatesHelper(@Nullable ReadableArray source) { + if (source == null) { + return null; + } + + JavaOnlyArray result = new JavaOnlyArray(); + for (int i = 0; i < source.size(); i++) { + switch (source.getType(i)) { + case Null: + result.pushNull(); + break; + case Boolean: + result.pushBoolean(source.getBoolean(i)); + break; + case Number: + result.pushDouble(source.getDouble(i)); + break; + case String: + result.pushString(source.getString(i)); + break; + case Map: + ReadableMap map = source.getMap(i); + if (map.hasKey(NODE_TAG_KEY) && map.getType(NODE_TAG_KEY) == ReadableType.Number) { + AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(map.getInt(NODE_TAG_KEY)); + if (node == null) { + throw new IllegalArgumentException("Mapped value node does not exist"); + } else if (node instanceof ValueAnimatedNode) { + ValueAnimatedNode valueAnimatedNode = (ValueAnimatedNode) node; + Object animatedObject = valueAnimatedNode.getAnimatedObject(); + if (animatedObject instanceof Integer) { + result.pushInt((Integer) animatedObject); + } else if (animatedObject instanceof String) { + result.pushString((String) animatedObject); + } else { + result.pushDouble(valueAnimatedNode.getValue()); + } + } else if (node instanceof ColorAnimatedNode) { + result.pushInt(((ColorAnimatedNode) node).getColor()); + } + } else { + result.pushMap(collectViewUpdatesHelper(source.getMap(i))); + } + break; + case Array: + result.pushArray(collectViewUpdatesHelper(source.getArray(i))); + break; + } + } + return result; + } + + private @Nullable JavaOnlyMap collectViewUpdatesHelper(@Nullable ReadableMap source) { + if (source == null) { + return null; + } + + JavaOnlyMap result = new JavaOnlyMap(); + ReadableMapKeySetIterator iter = source.keySetIterator(); + while (iter.hasNextKey()) { + String propKey = iter.nextKey(); + switch (source.getType(propKey)) { + case Null: + result.putNull(propKey); + break; + case Boolean: + result.putBoolean(propKey, source.getBoolean(propKey)); + break; + case Number: + result.putDouble(propKey, source.getDouble(propKey)); + break; + case String: + result.putString(propKey, source.getString(propKey)); + break; + case Map: + ReadableMap map = source.getMap(propKey); + if (map != null + && map.hasKey(NODE_TAG_KEY) + && map.getType(NODE_TAG_KEY) == ReadableType.Number) { + AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(map.getInt(NODE_TAG_KEY)); + if (node == null) { + throw new IllegalArgumentException("Mapped value node does not exist"); + } else if (node instanceof ValueAnimatedNode) { + ValueAnimatedNode valueAnimatedNode = (ValueAnimatedNode) node; + Object animatedObject = valueAnimatedNode.getAnimatedObject(); + if (animatedObject instanceof Integer) { + result.putInt(propKey, (Integer) animatedObject); + } else if (animatedObject instanceof String) { + result.putString(propKey, (String) animatedObject); + } else { + result.putDouble(propKey, valueAnimatedNode.getValue()); + } + } else if (node instanceof ColorAnimatedNode) { + result.putInt(propKey, ((ColorAnimatedNode) node).getColor()); + } + } else { + result.putMap(propKey, collectViewUpdatesHelper(map)); + } + break; + case Array: + result.putArray(propKey, collectViewUpdatesHelper(source.getArray(propKey))); + break; + } + } + return result; + } + + @Override + public String prettyPrint() { + return "ObjectAnimatedNode[" + + mTag + + "]: mConfig: " + + (mConfig != null ? mConfig.toString() : "null"); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java index b70f62c3c508a2..ff722adbb248af 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -111,6 +111,8 @@ public final void updateView() { } } else if (node instanceof ColorAnimatedNode) { mPropMap.putInt(entry.getKey(), ((ColorAnimatedNode) node).getColor()); + } else if (node instanceof ObjectAnimatedNode) { + ((ObjectAnimatedNode) node).collectViewUpdates(entry.getKey(), mPropMap); } else { throw new IllegalArgumentException( "Unsupported type of node used in property node " + node.getClass()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java index dbb377a14bde8c..ae4b70c215d9e7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java @@ -52,6 +52,8 @@ public void collectViewUpdates(JavaOnlyMap propsMap) { } } else if (node instanceof ColorAnimatedNode) { propsMap.putInt(entry.getKey(), ((ColorAnimatedNode) node).getColor()); + } else if (node instanceof ObjectAnimatedNode) { + ((ObjectAnimatedNode) node).collectViewUpdates(entry.getKey(), propsMap); } else { throw new IllegalArgumentException( "Unsupported type of node used in property node " + node.getClass());