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());