From 6d978c3c8b1e3929e59439db0c0a34dd094253cf Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 6 Sep 2016 15:16:16 -0700 Subject: [PATCH] Add support for extrapolation Summary: Adds support for the `extrapolate` parameter on the native interpolation node. This is pretty much a 1 to 1 port of the JS implementation. **Test plan** Tested by adding the `extrapolate` parameter in the native animated UIExplorer example. Closes https://github.com/facebook/react-native/pull/9366 Differential Revision: D3824154 fbshipit-source-id: 2ef593af827a8bd3d7b8ab2d53abbdc9516c6022 --- .../Animated/src/AnimatedImplementation.js | 9 +- .../Animated/src/NativeAnimatedHelper.js | 3 + .../animated/InterpolationAnimatedNode.java | 62 +++++++++++-- .../NativeAnimatedInterpolationTest.java | 89 ++++++++++++++----- .../NativeAnimatedNodeTraversalTest.java | 6 +- 5 files changed, 141 insertions(+), 28 deletions(-) diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index df08c066a..437b6e507 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1071,11 +1071,16 @@ class AnimatedInterpolation extends AnimatedWithChildren { } __getNativeConfig(): any { - NativeAnimatedHelper.validateInterpolation(this._config); + if (__DEV__) { + NativeAnimatedHelper.validateInterpolation(this._config); + } + return { - ...this._config, + inputRange: this._config.inputRange, // Only the `outputRange` can contain strings so we don't need to tranform `inputRange` here outputRange: this.__transformDataType(this._config.outputRange), + extrapolateLeft: this._config.extrapolateLeft || this._config.extrapolate || 'extend', + extrapolateRight: this._config.extrapolateRight || this._config.extrapolate || 'extend', type: 'interpolation', }; } diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 668a1e079..117ec61c8 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -135,6 +135,9 @@ function validateInterpolation(config: Object): void { var SUPPORTED_INTERPOLATION_PARAMS = { inputRange: true, outputRange: true, + extrapolate: true, + extrapolateRight: true, + extrapolateLeft: true, }; for (var key in config) { if (!SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(key)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java index 84ac32a3c..a673c0705 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java @@ -1,5 +1,6 @@ package com.facebook.react.animated; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -12,6 +13,10 @@ import javax.annotation.Nullable; */ /*package*/ class InterpolationAnimatedNode extends ValueAnimatedNode { + public static final String EXTRAPOLATE_TYPE_IDENTITY = "identity"; + public static final String EXTRAPOLATE_TYPE_CLAMP = "clamp"; + public static final String EXTRAPOLATE_TYPE_EXTEND = "extend"; + private static double[] fromDoubleArray(ReadableArray ary) { double[] res = new double[ary.size()]; for (int i = 0; i < res.length; i++) { @@ -25,19 +30,62 @@ import javax.annotation.Nullable; double inputMin, double inputMax, double outputMin, - double outputMax) { + double outputMax, + String extrapolateLeft, + String extrapolateRight) { + double result = value; + + // Extrapolate + if (result < inputMin) { + switch (extrapolateLeft) { + case EXTRAPOLATE_TYPE_IDENTITY: + return result; + case EXTRAPOLATE_TYPE_CLAMP: + result = inputMin; + break; + case EXTRAPOLATE_TYPE_EXTEND: + break; + default: + throw new JSApplicationIllegalArgumentException( + "Invalid extrapolation type " + extrapolateLeft + "for left extrapolation"); + } + } + + if (result > inputMax) { + switch (extrapolateRight) { + case EXTRAPOLATE_TYPE_IDENTITY: + return result; + case EXTRAPOLATE_TYPE_CLAMP: + result = inputMax; + break; + case EXTRAPOLATE_TYPE_EXTEND: + break; + default: + throw new JSApplicationIllegalArgumentException( + "Invalid extrapolation type " + extrapolateRight + "for right extrapolation"); + } + } + return outputMin + (outputMax - outputMin) * - (value - inputMin) / (inputMax - inputMin); + (result - inputMin) / (inputMax - inputMin); } - /*package*/ static double interpolate(double value, double[] inputRange, double[] outputRange) { + /*package*/ static double interpolate( + double value, + double[] inputRange, + double[] outputRange, + String extrapolateLeft, + String extrapolateRight + ) { int rangeIndex = findRangeIndex(value, inputRange); return interpolate( value, inputRange[rangeIndex], inputRange[rangeIndex + 1], outputRange[rangeIndex], - outputRange[rangeIndex + 1]); + outputRange[rangeIndex + 1], + extrapolateLeft, + extrapolateRight); } private static int findRangeIndex(double value, double[] ranges) { @@ -52,11 +100,15 @@ import javax.annotation.Nullable; private final double mInputRange[]; private final double mOutputRange[]; + private final String mExtrapolateLeft; + private final String mExtrapolateRight; private @Nullable ValueAnimatedNode mParent; public InterpolationAnimatedNode(ReadableMap config) { mInputRange = fromDoubleArray(config.getArray("inputRange")); mOutputRange = fromDoubleArray(config.getArray("outputRange")); + mExtrapolateLeft = config.getString("extrapolateLeft"); + mExtrapolateRight = config.getString("extrapolateRight"); } @Override @@ -84,6 +136,6 @@ import javax.annotation.Nullable; throw new IllegalStateException("Trying to update interpolation node that has not been " + "attached to the parent"); } - mValue = interpolate(mParent.mValue, mInputRange, mOutputRange); + mValue = interpolate(mParent.mValue, mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight); } } diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java index 492fa44be..27973a3ac 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java @@ -12,53 +12,102 @@ import static org.fest.assertions.api.Assertions.assertThat; @RunWith(RobolectricTestRunner.class) public class NativeAnimatedInterpolationTest { + private double simpleInterpolation(double value, double[] input, double[] output) { + return InterpolationAnimatedNode.interpolate( + value, + input, + output, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_EXTEND, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_EXTEND + ); + } + @Test public void testSimpleOneToOneMapping() { double[] input = new double[] {0d, 1d}; double[] output = new double[] {0d, 1d}; - assertThat(InterpolationAnimatedNode.interpolate(0, input, output)).isEqualTo(0); - assertThat(InterpolationAnimatedNode.interpolate(0.5, input, output)).isEqualTo(0.5); - assertThat(InterpolationAnimatedNode.interpolate(0.8, input, output)).isEqualTo(0.8); - assertThat(InterpolationAnimatedNode.interpolate(1, input, output)).isEqualTo(1); + assertThat(simpleInterpolation(0, input, output)).isEqualTo(0); + assertThat(simpleInterpolation(0.5, input, output)).isEqualTo(0.5); + assertThat(simpleInterpolation(0.8, input, output)).isEqualTo(0.8); + assertThat(simpleInterpolation(1, input, output)).isEqualTo(1); } @Test public void testWiderOutputRange() { double[] input = new double[] {0d, 1d}; double[] output = new double[] {100d, 200d}; - assertThat(InterpolationAnimatedNode.interpolate(0, input, output)).isEqualTo(100); - assertThat(InterpolationAnimatedNode.interpolate(0.5, input, output)).isEqualTo(150); - assertThat(InterpolationAnimatedNode.interpolate(0.8, input, output)).isEqualTo(180); - assertThat(InterpolationAnimatedNode.interpolate(1, input, output)).isEqualTo(200); + assertThat(simpleInterpolation(0, input, output)).isEqualTo(100); + assertThat(simpleInterpolation(0.5, input, output)).isEqualTo(150); + assertThat(simpleInterpolation(0.8, input, output)).isEqualTo(180); + assertThat(simpleInterpolation(1, input, output)).isEqualTo(200); } @Test public void testWiderInputRange() { double[] input = new double[] {2000d, 3000d}; double[] output = new double[] {1d, 2d}; - assertThat(InterpolationAnimatedNode.interpolate(2000, input, output)).isEqualTo(1); - assertThat(InterpolationAnimatedNode.interpolate(2250, input, output)).isEqualTo(1.25); - assertThat(InterpolationAnimatedNode.interpolate(2800, input, output)).isEqualTo(1.8); - assertThat(InterpolationAnimatedNode.interpolate(3000, input, output)).isEqualTo(2); + assertThat(simpleInterpolation(2000, input, output)).isEqualTo(1); + assertThat(simpleInterpolation(2250, input, output)).isEqualTo(1.25); + assertThat(simpleInterpolation(2800, input, output)).isEqualTo(1.8); + assertThat(simpleInterpolation(3000, input, output)).isEqualTo(2); } @Test public void testManySegments() { double[] input = new double[] {-1d, 1d, 5d}; double[] output = new double[] {0, 10d, 20d}; - assertThat(InterpolationAnimatedNode.interpolate(-1, input, output)).isEqualTo(0); - assertThat(InterpolationAnimatedNode.interpolate(0, input, output)).isEqualTo(5); - assertThat(InterpolationAnimatedNode.interpolate(1, input, output)).isEqualTo(10); - assertThat(InterpolationAnimatedNode.interpolate(2, input, output)).isEqualTo(12.5); - assertThat(InterpolationAnimatedNode.interpolate(5, input, output)).isEqualTo(20); + assertThat(simpleInterpolation(-1, input, output)).isEqualTo(0); + assertThat(simpleInterpolation(0, input, output)).isEqualTo(5); + assertThat(simpleInterpolation(1, input, output)).isEqualTo(10); + assertThat(simpleInterpolation(2, input, output)).isEqualTo(12.5); + assertThat(simpleInterpolation(5, input, output)).isEqualTo(20); } @Test - public void testExtrapolate() { + public void testExtendExtrapolate() { double[] input = new double[] {10d, 20d}; double[] output = new double[] {0d, 1d}; - assertThat(InterpolationAnimatedNode.interpolate(30d, input, output)).isEqualTo(2); - assertThat(InterpolationAnimatedNode.interpolate(5d, input, output)).isEqualTo(-0.5); + assertThat(simpleInterpolation(30d, input, output)).isEqualTo(2); + assertThat(simpleInterpolation(5d, input, output)).isEqualTo(-0.5); } + @Test + public void testClampExtrapolate() { + double[] input = new double[] {10d, 20d}; + double[] output = new double[] {0d, 1d}; + assertThat(InterpolationAnimatedNode.interpolate( + 30d, + input, + output, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_CLAMP, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_CLAMP + )).isEqualTo(1); + assertThat(InterpolationAnimatedNode.interpolate( + 5d, + input, + output, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_CLAMP, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_CLAMP + )).isEqualTo(0); + } + + @Test + public void testIdentityExtrapolate() { + double[] input = new double[] {10d, 20d}; + double[] output = new double[] {0d, 1d}; + assertThat(InterpolationAnimatedNode.interpolate( + 30d, + input, + output, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY + )).isEqualTo(30); + assertThat(InterpolationAnimatedNode.interpolate( + 5d, + input, + output, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY, + InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY + )).isEqualTo(5); + } } diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 2f5227d8e..db779ea29 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -652,7 +652,11 @@ public class NativeAnimatedNodeTraversalTest { "inputRange", JavaOnlyArray.of(10d, 20d), "outputRange", - JavaOnlyArray.of(0d, 1d))); + JavaOnlyArray.of(0d, 1d), + "extrapolateLeft", + "extend", + "extrapolateRight", + "extend")); mNativeAnimatedNodesManager.createAnimatedNode( 3,