From df053117778d8ebcc1943f20127e49078f79da0b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sun, 7 Aug 2016 00:44:09 -0700 Subject: [PATCH] Add transform support for native animated on Android Summary: This adds support for the `transform` animated node. This brings feature parity with the iOS implementation and allows running the NativeAnimated UIExplorer example that was created with the iOS implementation on Android. This is based on some work by kmagiera in the exponent RN fork. This also adds support for mixing static values with animated ones in the same transform as well which is not supported on iOS at the moment. It is also implemented in a way that rebuilds the transform matrix the same way as we build it in JS so it will be easy to remove some of the current limitations like forcing the transforms order and only supporting one of each type. **Test plan (required)** Tested with the NativeAnimated example on Android and iOS. Also tested mixing in static values in a transform (`[{ rotate: '45deg' }, { translateX: animatedValue }]`). Closes https://github.com/facebook/react-native/pull/8839 Differential Revision: D3682143 fbshipit-source-id: 5e6fd4b0b8be6a76053f24a36d1785771690a6f8 --- .../UIExplorer/js/NativeAnimationsExample.js | 2 +- .../UIExplorer/js/UIExplorerList.android.js | 12 ++- Examples/UIExplorer/js/UIExplorerList.ios.js | 8 +- .../Animated/src/AnimatedImplementation.js | 7 ++ .../Nodes/RCTTransformAnimatedNode.m | 6 ++ ...ddition.java => AdditionAnimatedNode.java} | 9 ++ .../animated/MultiplicationAnimatedNode.java | 9 ++ .../animated/NativeAnimatedNodesManager.java | 2 + .../react/animated/StyleAnimatedNode.java | 2 + .../react/animated/TransformAnimatedNode.java | 87 +++++++++++++++++++ .../facebook/react/bridge/JavaOnlyMap.java | 18 +++- .../react/uimanager/TransformHelper.java | 4 +- 12 files changed, 154 insertions(+), 12 deletions(-) rename ReactAndroid/src/main/java/com/facebook/react/animated/{AnimatedAddition.java => AdditionAnimatedNode.java} (82%) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animated/TransformAnimatedNode.java diff --git a/Examples/UIExplorer/js/NativeAnimationsExample.js b/Examples/UIExplorer/js/NativeAnimationsExample.js index bd7add201..b4c151c53 100644 --- a/Examples/UIExplorer/js/NativeAnimationsExample.js +++ b/Examples/UIExplorer/js/NativeAnimationsExample.js @@ -335,7 +335,7 @@ exports.examples = [ inputRange: [0, 1], outputRange: [0, 100], }) - } + }, ], } ]} diff --git a/Examples/UIExplorer/js/UIExplorerList.android.js b/Examples/UIExplorer/js/UIExplorerList.android.js index 58b4f7b04..f35a34db4 100644 --- a/Examples/UIExplorer/js/UIExplorerList.android.js +++ b/Examples/UIExplorer/js/UIExplorerList.android.js @@ -50,10 +50,6 @@ var ComponentExamples: Array = [ key: 'ModalExample', module: require('./ModalExample'), }, - { - key: 'NativeAnimationsExample', - module: require('./NativeAnimationsExample'), - }, { key: 'PickerExample', module: require('./PickerExample'), @@ -121,6 +117,10 @@ const APIExamples = [ key: 'AlertExample', module: require('./AlertExample').AlertExample, }, + { + key: 'AnimatedExample', + module: require('./AnimatedExample'), + }, { key: 'AppStateExample', module: require('./AppStateExample'), @@ -165,6 +165,10 @@ const APIExamples = [ key: 'LayoutExample', module: require('./LayoutExample'), }, + { + key: 'NativeAnimationsExample', + module: require('./NativeAnimationsExample'), + }, { key: 'NavigationExperimentalExample', module: require('./NavigationExperimental/NavigationExperimentalExample'), diff --git a/Examples/UIExplorer/js/UIExplorerList.ios.js b/Examples/UIExplorer/js/UIExplorerList.ios.js index 044c770da..2e996c065 100644 --- a/Examples/UIExplorer/js/UIExplorerList.ios.js +++ b/Examples/UIExplorer/js/UIExplorerList.ios.js @@ -68,10 +68,6 @@ const ComponentExamples: Array = [ key: 'ModalExample', module: require('./ModalExample'), }, - { - key: 'NativeAnimationsExample', - module: require('./NativeAnimationsExample'), - }, { key: 'NavigatorExample', module: require('./Navigator/NavigatorExample'), @@ -227,6 +223,10 @@ const APIExamples: Array = [ key: 'LinkingExample', module: require('./LinkingExample'), }, + { + key: 'NativeAnimationsExample', + module: require('./NativeAnimationsExample'), + }, { key: 'NavigationExperimentalExample', module: require('./NavigationExperimental/NavigationExperimentalExample'), diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 02c984f83..f21ed5be2 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1251,9 +1251,16 @@ class AnimatedTransform extends AnimatedWithChildren { var value = transform[key]; if (value instanceof Animated) { transConfigs.push({ + type: 'animated', property: key, nodeTag: value.__getNativeTag(), }); + } else { + transConfigs.push({ + type: 'static', + property: key, + value, + }); } } }); diff --git a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m index 2f2799ff8..d44fe4d7c 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m @@ -37,6 +37,12 @@ NSArray *transformConfigs = self.config[@"transforms"]; for (NSDictionary *transformConfig in transformConfigs) { + NSString *type = transformConfig[@"type"]; + // TODO: Support static transform values. + if (![type isEqualToString: @"animated"]) { + continue; + } + NSNumber *nodeTag = transformConfig[@"nodeTag"]; RCTAnimatedNode *node = self.parentNodes[nodeTag]; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedAddition.java b/ReactAndroid/src/main/java/com/facebook/react/animated/AdditionAnimatedNode.java similarity index 82% rename from ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedAddition.java rename to ReactAndroid/src/main/java/com/facebook/react/animated/AdditionAnimatedNode.java index 3c538e08d..34b96be57 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedAddition.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/AdditionAnimatedNode.java @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + package com.facebook.react.animated; import com.facebook.react.bridge.JSApplicationCausedNativeException; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/MultiplicationAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/MultiplicationAnimatedNode.java index 9fe8d7a81..5838ed0c5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/MultiplicationAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/MultiplicationAnimatedNode.java @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + package com.facebook.react.animated; import com.facebook.react.bridge.JSApplicationCausedNativeException; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index deb236155..67a735e16 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -78,6 +78,8 @@ import javax.annotation.Nullable; node = new AdditionAnimatedNode(config, this); } else if ("multiplication".equals(type)) { node = new MultiplicationAnimatedNode(config, this); + } else if ("transform".equals(type)) { + node = new TransformAnimatedNode(config, this); } else { throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java index de7c1b054..f23cf248a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java @@ -43,6 +43,8 @@ import javax.annotation.Nullable; @Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); if (node == null) { throw new IllegalArgumentException("Mapped style node does not exists"); + } else if (node instanceof TransformAnimatedNode) { + ((TransformAnimatedNode) node).collectViewUpdates(propsMap); } else if (node instanceof ValueAnimatedNode) { propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).mValue); } else { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/TransformAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/TransformAnimatedNode.java new file mode 100644 index 000000000..69213dcde --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/TransformAnimatedNode.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.animated; + +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 java.util.ArrayList; +import java.util.List; + +/** + * Native counterpart of transform animated node (see AnimatedTransform class in AnimatedImplementation.js) + */ +/* package */ class TransformAnimatedNode extends AnimatedNode { + + private class TransformConfig { + public String mProperty; + } + + private class AnimatedTransformConfig extends TransformConfig { + public int mNodeTag; + } + + private class StaticTransformConfig extends TransformConfig { + public double mValue; + } + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final List mTransformConfigs; + + TransformAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { + ReadableArray transforms = config.getArray("transforms"); + mTransformConfigs = new ArrayList<>(transforms.size()); + for (int i = 0; i < transforms.size(); i++) { + ReadableMap transformConfigMap = transforms.getMap(i); + String property = transformConfigMap.getString("property"); + String type = transformConfigMap.getString("type"); + if (type.equals("animated")) { + AnimatedTransformConfig transformConfig = new AnimatedTransformConfig(); + transformConfig.mProperty = property; + transformConfig.mNodeTag = transformConfigMap.getInt("nodeTag"); + mTransformConfigs.add(transformConfig); + } else { + StaticTransformConfig transformConfig = new StaticTransformConfig(); + transformConfig.mProperty = property; + transformConfig.mValue = transformConfigMap.getDouble("value"); + mTransformConfigs.add(transformConfig); + } + } + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + } + + public void collectViewUpdates(JavaOnlyMap propsMap) { + List transforms = new ArrayList<>(mTransformConfigs.size()); + + for (TransformConfig transformConfig : mTransformConfigs) { + double value; + if (transformConfig instanceof AnimatedTransformConfig) { + int nodeTag = ((AnimatedTransformConfig) transformConfig).mNodeTag; + AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(nodeTag); + if (node == null) { + throw new IllegalArgumentException("Mapped style node does not exists"); + } else if (node instanceof ValueAnimatedNode) { + value = ((ValueAnimatedNode) node).mValue; + } else { + throw new IllegalArgumentException("Unsupported type of node used as a transform child " + + "node " + node.getClass()); + } + } else { + value = ((StaticTransformConfig) transformConfig).mValue; + } + + transforms.add(JavaOnlyMap.of(transformConfig.mProperty, value)); + } + + propsMap.putArray("transform", JavaOnlyArray.from(transforms)); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index 607d686e2..c9e7c3114 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -90,7 +90,23 @@ public class JavaOnlyMap implements ReadableMap, WritableMap { @Override public ReadableType getType(String name) { - throw new UnsupportedOperationException("Method not implemented"); + Object value = mBackingMap.get(name); + if (value == null) { + return ReadableType.Null; + } else if (value instanceof Number) { + return ReadableType.Number; + } else if (value instanceof String) { + return ReadableType.String; + } else if (value instanceof Boolean) { + return ReadableType.Boolean; + } else if (value instanceof ReadableMap) { + return ReadableType.Map; + } else if (value instanceof ReadableArray) { + return ReadableType.Array; + } else { + throw new IllegalArgumentException("Invalid value " + value.toString() + " for key " + name + + "contained in JavaOnlyMap"); + } } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java index c233d9407..52895d4b7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.java @@ -20,13 +20,13 @@ public class TransformHelper { private static double convertToRadians(ReadableMap transformMap, String key) { double value; - boolean inRadians = false; + boolean inRadians = true; if (transformMap.getType(key) == ReadableType.String) { String stringValue = transformMap.getString(key); if (stringValue.endsWith("rad")) { - inRadians = true; stringValue = stringValue.substring(0, stringValue.length() - 3); } else if (stringValue.endsWith("deg")) { + inRadians = false; stringValue = stringValue.substring(0, stringValue.length() - 3); } value = Float.parseFloat(stringValue);