From 4906f8d28cdbaf1b432494b473a4c61c1e193881 Mon Sep 17 00:00:00 2001 From: Vladislav Pilgasov Date: Fri, 30 Mar 2018 20:57:42 -0700 Subject: [PATCH] Add an implementation of Animated.subtract Summary: Fixes #18451 I've added another example to NativeAnimationsExample, which makes use of `Animated.substract()`, let me know if the example is not desired / doesn't add much value. Below two GIFs of the new method working on iOS and Android: https://github.com/facebook/react-native-website/pull/276 [GENERAL] [ENHANCEMENT] [Animated] - Implemented Animated.subtract Closes https://github.com/facebook/react-native/pull/18630 Differential Revision: D7462867 Pulled By: hramos fbshipit-source-id: 4cb0b8af08bb0c841e44ea2099889b8c02a22a4a --- .../Animated/src/AnimatedImplementation.js | 16 +++++ .../src/__tests__/AnimatedNative-test.js | 32 ++++++++++ .../Animated/src/nodes/AnimatedSubtraction.js | 63 +++++++++++++++++++ .../Nodes/RCTSubtractionAnimatedNode.h | 13 ++++ .../Nodes/RCTSubtractionAnimatedNode.m | 27 ++++++++ .../RCTAnimation.xcodeproj/project.pbxproj | 6 ++ .../RCTNativeAnimatedNodesManager.m | 2 + RNTester/js/NativeAnimationsExample.js | 42 +++++++++++++ .../animated/NativeAnimatedNodesManager.java | 2 + .../animated/SubtractionAnimatedNode.java | 51 +++++++++++++++ 10 files changed, 254 insertions(+) create mode 100644 Libraries/Animated/src/nodes/AnimatedSubtraction.js create mode 100644 Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.m create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animated/SubtractionAnimatedNode.java diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index e13aef1e7..9291a7956 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -20,6 +20,7 @@ const AnimatedModulo = require('./nodes/AnimatedModulo'); const AnimatedMultiplication = require('./nodes/AnimatedMultiplication'); const AnimatedNode = require('./nodes/AnimatedNode'); const AnimatedProps = require('./nodes/AnimatedProps'); +const AnimatedSubtraction = require('./nodes/AnimatedSubtraction'); const AnimatedTracking = require('./nodes/AnimatedTracking'); const AnimatedValue = require('./nodes/AnimatedValue'); const AnimatedValueXY = require('./nodes/AnimatedValueXY'); @@ -54,6 +55,13 @@ const add = function( return new AnimatedAddition(a, b); }; +const subtract = function( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedSubtraction { + return new AnimatedSubtraction(a, b); +}; + const divide = function( a: AnimatedNode | number, b: AnimatedNode | number, @@ -568,6 +576,14 @@ module.exports = { */ add, + /** + * Creates a new Animated value composed by subtracting the second Animated + * value from the first Animated value. + * + * See http://facebook.github.io/react-native/docs/animated.html#subtract + */ + subtract, + /** * Creates a new Animated value composed by dividing the first Animated value * by the second Animated value. diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index 1c6700e4d..f04d5afb1 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -333,6 +333,38 @@ describe('Native Animated', () => { .toBeCalledWith(additionCall[1].input[1], {type: 'value', value: 2, offset: 0}); }); + it('sends a valid graph description for Animated.subtract nodes', () => { + const first = new Animated.Value(2); + const second = new Animated.Value(1); + first.__makeNative(); + second.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.subtract(first, second), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + expect.any(Number), + {type: 'subtraction', input: expect.any(Array)}, + ); + const subtractionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'subtraction' + ); + expect(subtractionCalls.length).toBe(1); + const subtractionCall = subtractionCalls[0]; + const subtractionNodeTag = subtractionCall[0]; + const subtractionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === subtractionNodeTag + ); + expect(subtractionConnectionCalls.length).toBe(2); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(subtractionCall[1].input[0], {type: 'value', value: 2, offset: 0}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(subtractionCall[1].input[1], {type: 'value', value: 1, offset: 0}); + }); + it('sends a valid graph description for Animated.multiply nodes', () => { const first = new Animated.Value(2); const second = new Animated.Value(1); diff --git a/Libraries/Animated/src/nodes/AnimatedSubtraction.js b/Libraries/Animated/src/nodes/AnimatedSubtraction.js new file mode 100644 index 000000000..610fc226e --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedSubtraction.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnimatedSubtraction + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedSubtraction extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return this._a.__getValue() - this._b.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'subtraction', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedSubtraction; diff --git a/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.h new file mode 100644 index 000000000..45524b69d --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTValueAnimatedNode.h" + +@interface RCTSubtractionAnimatedNode : RCTValueAnimatedNode + +@end + diff --git a/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.m new file mode 100644 index 000000000..91688c9ae --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTSubtractionAnimatedNode.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTSubtractionAnimatedNode.h" + +@implementation RCTSubtractionAnimatedNode + +- (void)performUpdate +{ + [super performUpdate]; + NSArray *inputNodes = self.config[@"input"]; + if (inputNodes.count > 1) { + RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:inputNodes[0]]; + RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:inputNodes[1]]; + if ([parent1 isKindOfClass:[RCTValueAnimatedNode class]] && + [parent2 isKindOfClass:[RCTValueAnimatedNode class]]) { + self.value = parent1.value - parent2.value; + } + } +} + +@end + diff --git a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj index 0ba244627..08a629eae 100644 --- a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj +++ b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ 2D3B5EFE1D9B0B4800451313 /* RCTStyleAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */; }; 2D3B5EFF1D9B0B4800451313 /* RCTTransformAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */; }; 2D3B5F001D9B0B4800451313 /* RCTValueAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */; }; + 2EC00631206EA19300586E91 /* RCTSubtractionAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EC00630206EA19300586E91 /* RCTSubtractionAnimatedNode.m */; }; 44DB7D942024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */; }; 44DB7D952024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */; }; 44DB7D972024F75100588FCD /* RCTTrackingAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */; }; @@ -213,6 +214,8 @@ 19F00F201DC8847500113FEE /* RCTEventAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTEventAnimation.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 19F00F211DC8847500113FEE /* RCTEventAnimation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventAnimation.m; sourceTree = ""; }; 2D2A28201D9B03D100D4039D /* libRCTAnimation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAnimation.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 2EC0062F206EA15F00586E91 /* RCTSubtractionAnimatedNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTSubtractionAnimatedNode.h; sourceTree = ""; }; + 2EC00630206EA19300586E91 /* RCTSubtractionAnimatedNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTSubtractionAnimatedNode.m; sourceTree = ""; }; 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTrackingAnimatedNode.h; sourceTree = ""; }; 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTTrackingAnimatedNode.m; sourceTree = ""; }; 5C9894931D999639008027DB /* RCTDivisionAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTDivisionAnimatedNode.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -246,6 +249,8 @@ 193F64F31D776EC6004D1CAA /* RCTDiffClampAnimatedNode.m */, 13E501D61D07A6C9005F35D8 /* RCTAdditionAnimatedNode.h */, 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */, + 2EC0062F206EA15F00586E91 /* RCTSubtractionAnimatedNode.h */, + 2EC00630206EA19300586E91 /* RCTSubtractionAnimatedNode.m */, 13E501D81D07A6C9005F35D8 /* RCTAnimatedNode.h */, 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */, 13E501DC1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.h */, @@ -483,6 +488,7 @@ 5C9894951D999639008027DB /* RCTDivisionAnimatedNode.m in Sources */, 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */, 194804EE1E975D8E00623005 /* RCTDecayAnimation.m in Sources */, + 2EC00631206EA19300586E91 /* RCTSubtractionAnimatedNode.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m index e05286dec..e649020d5 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m @@ -23,6 +23,7 @@ #import "RCTPropsAnimatedNode.h" #import "RCTSpringAnimation.h" #import "RCTStyleAnimatedNode.h" +#import "RCTSubtractionAnimatedNode.h" #import "RCTTransformAnimatedNode.h" #import "RCTValueAnimatedNode.h" #import "RCTTrackingAnimatedNode.h" @@ -66,6 +67,7 @@ @"division" : [RCTDivisionAnimatedNode class], @"multiplication" : [RCTMultiplicationAnimatedNode class], @"modulus" : [RCTModuloAnimatedNode class], + @"subtraction" : [RCTSubtractionAnimatedNode class], @"transform" : [RCTTransformAnimatedNode class], @"tracking" : [RCTTrackingAnimatedNode class]}; }); diff --git a/RNTester/js/NativeAnimationsExample.js b/RNTester/js/NativeAnimationsExample.js index 983b0c9d2..5a6e5df55 100644 --- a/RNTester/js/NativeAnimationsExample.js +++ b/RNTester/js/NativeAnimationsExample.js @@ -429,6 +429,48 @@ exports.examples = [ ); }, }, + { + title: 'Multistage With Subtract', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, { title: 'Scale interpolation with clamping', render: function() { 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 1fad4e52d..07357fd7e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -93,6 +93,8 @@ import javax.annotation.Nullable; node = new InterpolationAnimatedNode(config); } else if ("addition".equals(type)) { node = new AdditionAnimatedNode(config, this); + } else if ("subtraction".equals(type)) { + node = new SubtractionAnimatedNode(config, this); } else if ("division".equals(type)) { node = new DivisionAnimatedNode(config, this); } else if ("multiplication".equals(type)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/SubtractionAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/SubtractionAnimatedNode.java new file mode 100644 index 000000000..4a8ecd8ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/SubtractionAnimatedNode.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * 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 com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +/** + * Animated node that plays a role of value aggregator. It takes two or more value nodes as an input + * and outputs a difference of values outputted by those nodes. + */ +/*package*/ class SubtractionAnimatedNode extends ValueAnimatedNode { + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final int[] mInputNodes; + + public SubtractionAnimatedNode( + ReadableMap config, + NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + ReadableArray inputNodes = config.getArray("input"); + mInputNodes = new int[inputNodes.size()]; + for (int i = 0; i < mInputNodes.length; i++) { + mInputNodes[i] = inputNodes.getInt(i); + } + } + + @Override + public void update() { + for (int i = 0; i < mInputNodes.length; i++) { + AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mInputNodes[i]); + if (animatedNode != null && animatedNode instanceof ValueAnimatedNode) { + double value = ((ValueAnimatedNode) animatedNode).getValue(); + if (i == 0) { + mValue = value; + continue; + } + mValue -= ((ValueAnimatedNode) animatedNode).getValue(); + } else { + throw new JSApplicationCausedNativeException("Illegal node ID set as an input for " + + "Animated.subtract node"); + } + } + } +}