From cfcdc8101697a4d569d5100f35edd4e93c546946 Mon Sep 17 00:00:00 2001 From: Dylan Vann Date: Mon, 19 Feb 2018 01:57:15 -0500 Subject: [PATCH] Allow animated output range. --- .../Animated/src/AnimatedImplementation.js | 41 ++++++ .../src/__tests__/AnimatedNative-test.js | 3 +- .../Animated/src/nodes/AnimatedAddition.js | 7 - .../Animated/src/nodes/AnimatedDiffClamp.js | 7 - .../Animated/src/nodes/AnimatedDivision.js | 7 - .../src/nodes/AnimatedInterpolation.js | 91 ++++++++----- .../Animated/src/nodes/AnimatedModulo.js | 7 - .../src/nodes/AnimatedMultiplication.js | 7 - Libraries/Animated/src/nodes/AnimatedNode.js | 16 +++ Libraries/Animated/src/nodes/AnimatedValue.js | 21 +-- .../Nodes/RCTInterpolationAnimatedNode.m | 46 +++---- Libraries/NativeAnimation/RCTAnimationUtils.h | 2 + Libraries/NativeAnimation/RCTAnimationUtils.m | 4 +- RNTester/js/NativeAnimationsExample.js | 113 ++++++++++++++++ .../animated/InterpolationAnimatedNode.java | 99 ++++++++------ .../animated/NativeAnimatedNodesManager.java | 2 +- .../NativeAnimatedInterpolationTest.java | 123 ++++++++++++++++-- .../NativeAnimatedNodeTraversalTest.java | 17 ++- 18 files changed, 458 insertions(+), 155 deletions(-) diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 9291a7956531e3..f24c2feaf29385 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -39,6 +39,34 @@ import type {TimingAnimationConfig} from './animations/TimingAnimation'; import type {DecayAnimationConfig} from './animations/DecayAnimation'; import type {SpringAnimationConfig} from './animations/SpringAnimation'; import type {Mapping, EventConfig} from './AnimatedEvent'; +import type {InterpolationConfigType} from './nodes/AnimatedInterpolation'; + +const interpolateMethod = function( + config: InterpolationConfigType, +): AnimatedInterpolation { + console.warn( + 'The animation.interpolate(config) method will be removed from animated nodes in favour of Animated.interpolate(animation, config).', + ); + return new AnimatedInterpolation(this, config); +}; + +// To avoid some code duplication and a circular dependency we +// are adding the interpolate method directly onto these prototypes. +// This should eventually be removed. +//$FlowFixMe +AnimatedAddition.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedDiffClamp.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedDivision.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedInterpolation.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedModulo.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedMultiplication.prototype.interpolate = interpolateMethod; +//$FlowFixMe +AnimatedValue.prototype.interpolate = interpolateMethod; export type CompositeAnimation = { start: (callback?: ?EndCallback) => void, @@ -233,6 +261,13 @@ const timing = function( ); }; +const interpolate = function( + value: AnimatedValue, + config: InterpolationConfigType, +): AnimatedInterpolation { + return new AnimatedInterpolation(value, config); +}; + const decay = function( value: AnimatedValue | AnimatedValueXY, config: DecayAnimationConfig, @@ -546,6 +581,12 @@ module.exports = { */ Node: AnimatedNode, + /** + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. + */ + interpolate, + /** * Animates a value from an initial velocity to zero based on a decay * coefficient. diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index f04d5afb1429a3..18bd83c2087239 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -475,8 +475,9 @@ describe('Native Animated', () => { expect(nativeAnimatedModule.createAnimatedNode) .toBeCalledWith(expect.any(Number), { type: 'interpolation', + parent: expect.any(Number), inputRange: [10, 20], - outputRange: [0, 1], + outputRange: [expect.any(Number), expect.any(Number)], extrapolateLeft: 'extend', extrapolateRight: 'extend', }); diff --git a/Libraries/Animated/src/nodes/AnimatedAddition.js b/Libraries/Animated/src/nodes/AnimatedAddition.js index 2e3453965565f0..c750db3243bf92 100644 --- a/Libraries/Animated/src/nodes/AnimatedAddition.js +++ b/Libraries/Animated/src/nodes/AnimatedAddition.js @@ -10,13 +10,10 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedNode = require('./AnimatedNode'); const AnimatedValue = require('./AnimatedValue'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -import type {InterpolationConfigType} from './AnimatedInterpolation'; - class AnimatedAddition extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; @@ -37,10 +34,6 @@ class AnimatedAddition extends AnimatedWithChildren { 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); diff --git a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js index 0f64e75c723c06..417d618e1f957e 100644 --- a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js +++ b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js @@ -10,12 +10,9 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedNode = require('./AnimatedNode'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -import type {InterpolationConfigType} from './AnimatedInterpolation'; - class AnimatedDiffClamp extends AnimatedWithChildren { _a: AnimatedNode; _min: number; @@ -37,10 +34,6 @@ class AnimatedDiffClamp extends AnimatedWithChildren { super.__makeNative(); } - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - __getValue(): number { const value = this._a.__getValue(); const diff = value - this._lastValue; diff --git a/Libraries/Animated/src/nodes/AnimatedDivision.js b/Libraries/Animated/src/nodes/AnimatedDivision.js index 606e63937b9a86..e49bd030a2437e 100644 --- a/Libraries/Animated/src/nodes/AnimatedDivision.js +++ b/Libraries/Animated/src/nodes/AnimatedDivision.js @@ -10,13 +10,10 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedNode = require('./AnimatedNode'); const AnimatedValue = require('./AnimatedValue'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -import type {InterpolationConfigType} from './AnimatedInterpolation'; - class AnimatedDivision extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; @@ -42,10 +39,6 @@ class AnimatedDivision extends AnimatedWithChildren { return a / b; } - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - __attach(): void { this._a.__addChild(this); this._b.__addChild(this); diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js index bc7b1365879db8..72bf5d2f4c5e27 100644 --- a/Libraries/Animated/src/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -12,6 +12,7 @@ 'use strict'; const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); const AnimatedWithChildren = require('./AnimatedWithChildren'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); @@ -26,7 +27,7 @@ export type InterpolationConfigType = { * detected during the deployment of v0.38.0. To see the error, remove this * comment and run flow */ - outputRange: Array | Array, + outputRange: Array | Array | Array, easing?: (input: number) => number, extrapolate?: ExtrapolateType, extrapolateLeft?: ExtrapolateType, @@ -46,7 +47,7 @@ function createInterpolation( return createInterpolationFromStringOutputRange(config); } - const outputRange: Array = (config.outputRange: any); + const outputRange: Array | Array = config.outputRange; checkInfiniteRange('outputRange', outputRange); const inputRange = config.inputRange; @@ -85,12 +86,20 @@ function createInterpolation( ); const range = findRange(input, inputRange); + const outputStart: number | AnimatedNode = outputRange[range]; + const outputEnd: number | AnimatedNode = outputRange[range + 1]; + const outputStartValue = + outputStart instanceof AnimatedNode + ? outputStart.__getValue() + : outputStart; + const outputEndValue = + outputEnd instanceof AnimatedNode ? outputEnd.__getValue() : outputEnd; return interpolate( input, inputRange[range], inputRange[range + 1], - outputRange[range], - outputRange[range + 1], + outputStartValue, + outputEndValue, easing, extrapolateLeft, extrapolateRight, @@ -291,7 +300,7 @@ function checkValidInputRange(arr: Array) { } } -function checkInfiniteRange(name: string, arr: Array) { +function checkInfiniteRange(name: string, arr: Array) { invariant(arr.length >= 2, name + ' must have at least 2 elements'); invariant( arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity, @@ -311,17 +320,24 @@ class AnimatedInterpolation extends AnimatedWithChildren { _parent: AnimatedNode; _config: InterpolationConfigType; + _transformedOutputRange: Array; _interpolation: (input: number) => number | string; constructor(parent: AnimatedNode, config: InterpolationConfigType) { super(); this._parent = parent; this._config = config; + this._transformedOutputRange = this.__transformOutputRangeToAnimatedValues( + config.outputRange, + ); this._interpolation = createInterpolation(config); } - __makeNative() { + __makeNative(): void { this._parent.__makeNative(); + this._transformedOutputRange.forEach(function(value) { + value.__makeNative(); + }); super.__makeNative(); } @@ -334,37 +350,54 @@ class AnimatedInterpolation extends AnimatedWithChildren { return this._interpolation(parentValue); } - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - __attach(): void { - this._parent.__addChild(this); + const that = this; + this._parent.__addChild(that); + this._transformedOutputRange.forEach(function(value) { + value.__addChild(that); + }); } __detach(): void { - this._parent.__removeChild(this); + const that = this; + this._parent.__removeChild(that); + this._transformedOutputRange.forEach(function(value) { + value.__removeChild(that); + }); super.__detach(); } - __transformDataType(range: Array) { - // Change the string array type to number array - // So we can reuse the same logic in iOS and Android platform - /* $FlowFixMe(>=0.70.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.70 was deployed. To see the error delete this - * comment and run Flow. */ + __transformOutputRangeToAnimatedValues( + range: Array, + ): Array { return range.map(function(value) { - if (typeof value !== 'string') { - return value; - } - if (/deg$/.test(value)) { + if (typeof value === 'string' && /deg$/.test(value)) { const degrees = parseFloat(value) || 0; + // Radians. const radians = degrees * Math.PI / 180.0; - return radians; - } else { - // Assume radians - return parseFloat(value) || 0; + return new AnimatedValue(radians); + } + if (typeof value === 'string') { + // Assume radians. + const radians = parseFloat(value) || 0; + return new AnimatedValue(radians); } + if (typeof value === 'number') { + // Just a plain number value. + return new AnimatedValue(value); + } + if (value instanceof AnimatedNode) { + return value; + } + throw new Error('Incompatible type passed to outputRange.'); + }); + } + + __outputRangeToTags(range: Array): Array { + return range.map(function(value) { + const tag = value.__getNativeTag(); + invariant(tag, 'There must be a native tag for this value.'); + return tag; }); } @@ -374,14 +407,14 @@ class AnimatedInterpolation extends AnimatedWithChildren { } return { + type: 'interpolation', + parent: this._parent.__getNativeTag(), inputRange: this._config.inputRange, - // Only the `outputRange` can contain strings so we don't need to transform `inputRange` here - outputRange: this.__transformDataType(this._config.outputRange), + outputRange: this.__outputRangeToTags(this._transformedOutputRange), extrapolateLeft: this._config.extrapolateLeft || this._config.extrapolate || 'extend', extrapolateRight: this._config.extrapolateRight || this._config.extrapolate || 'extend', - type: 'interpolation', }; } } diff --git a/Libraries/Animated/src/nodes/AnimatedModulo.js b/Libraries/Animated/src/nodes/AnimatedModulo.js index 24cd3f0b077c12..cba2c49efb145b 100644 --- a/Libraries/Animated/src/nodes/AnimatedModulo.js +++ b/Libraries/Animated/src/nodes/AnimatedModulo.js @@ -10,12 +10,9 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedNode = require('./AnimatedNode'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -import type {InterpolationConfigType} from './AnimatedInterpolation'; - class AnimatedModulo extends AnimatedWithChildren { _a: AnimatedNode; _modulus: number; @@ -37,10 +34,6 @@ class AnimatedModulo extends AnimatedWithChildren { ); } - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - __attach(): void { this._a.__addChild(this); } diff --git a/Libraries/Animated/src/nodes/AnimatedMultiplication.js b/Libraries/Animated/src/nodes/AnimatedMultiplication.js index dc01f7f17127f1..d4869ac6b50be1 100644 --- a/Libraries/Animated/src/nodes/AnimatedMultiplication.js +++ b/Libraries/Animated/src/nodes/AnimatedMultiplication.js @@ -10,13 +10,10 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedNode = require('./AnimatedNode'); const AnimatedValue = require('./AnimatedValue'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -import type {InterpolationConfigType} from './AnimatedInterpolation'; - class AnimatedMultiplication extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; @@ -37,10 +34,6 @@ class AnimatedMultiplication extends AnimatedWithChildren { 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); diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/src/nodes/AnimatedNode.js index e75cfaf0e5c8fb..bc588f28621055 100644 --- a/Libraries/Animated/src/nodes/AnimatedNode.js +++ b/Libraries/Animated/src/nodes/AnimatedNode.js @@ -34,6 +34,22 @@ class AnimatedNode { return []; } + /** + * Deprecated - Use `Animated.interpolate(animation, config)` instead. + * + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. Not available on all node types. + * + * @deprecated + */ + interpolate(config: any): AnimatedNode { + throw new Error( + 'This node type does not implement an interpolate method,' + + ' the interpolate method will be removed from all nodes' + + ' in favour of Animated.interpolate(animation, config).', + ); + } + /* Methods and props used by native Animated impl */ __isNative: boolean; __nativeTag: ?number; diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index 9d06c751cefbea..63bce0fd501ca7 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -10,14 +10,11 @@ */ 'use strict'; -const AnimatedInterpolation = require('./AnimatedInterpolation'); -const AnimatedNode = require('./AnimatedNode'); const AnimatedWithChildren = require('./AnimatedWithChildren'); const InteractionManager = require('InteractionManager'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); import type Animation, {EndCallback} from '../animations/Animation'; -import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedTracking from './AnimatedTracking'; const NativeAnimatedAPI = NativeAnimatedHelper.API; @@ -31,11 +28,11 @@ let _uniqueId = 1; * transparently when you render your Animated components. * * new Animated.Value(0) - * .interpolate() .interpolate() new Animated.Value(1) - * opacity translateY scale - * style transform - * View#234 style - * View#123 + * Animated.interpolate() Animated.interpolate() new Animated.Value(1) + * opacity translateY scale + * style transform + * View#234 style + * View#123 * * A) Top Down phase * When an Animated.Value is updated, we recursively go down through this @@ -261,14 +258,6 @@ class AnimatedValue extends AnimatedWithChildren { this._value = this._startingValue; } - /** - * Interpolates the value before updating the property, e.g. mapping 0-1 to - * 0-10. - */ - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - /** * Typically only used internally, but could be used by a custom Animation * class. diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m index b7fa8b50974d23..d48b449facad95 100644 --- a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m @@ -36,36 +36,36 @@ - (instancetype)initWithTag:(NSNumber *)tag return self; } -- (void)onAttachedToNode:(RCTAnimatedNode *)parent -{ - [super onAttachedToNode:parent]; - if ([parent isKindOfClass:[RCTValueAnimatedNode class]]) { - _parentNode = (RCTValueAnimatedNode *)parent; - } -} - -- (void)onDetachedFromNode:(RCTAnimatedNode *)parent -{ - [super onDetachedFromNode:parent]; - if (_parentNode == parent) { - _parentNode = nil; - } -} - - (void)performUpdate { [super performUpdate]; + _parentNode = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:self.config[@"parent"]]; if (!_parentNode) { return; } - + CGFloat inputValue = _parentNode.value; - - self.value = RCTInterpolateValueInRange(inputValue, - _inputRange, - _outputRange, - _extrapolateLeft, - _extrapolateRight); + + NSUInteger rangeIndex = RCTFindIndexOfNearestValue(inputValue, _inputRange); + CGFloat inputMin = _inputRange[rangeIndex].doubleValue; + CGFloat inputMax = _inputRange[rangeIndex + 1].doubleValue; + + NSNumber *minTag = _outputRange[rangeIndex]; + RCTValueAnimatedNode *outputMin = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:minTag]; + + NSNumber *maxTag = _outputRange[rangeIndex + 1]; + RCTValueAnimatedNode *outputMax = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:maxTag]; + + CGFloat outputMinValue = outputMin.value; + CGFloat outputMaxValue = outputMax.value; + + self.value = RCTInterpolateValue(inputValue, + inputMin, + inputMax, + outputMinValue, + outputMaxValue, + _extrapolateLeft, + _extrapolateRight); } @end diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.h b/Libraries/NativeAnimation/RCTAnimationUtils.h index d1dba830ee96aa..1bb66c3e14a59f 100644 --- a/Libraries/NativeAnimation/RCTAnimationUtils.h +++ b/Libraries/NativeAnimation/RCTAnimationUtils.h @@ -14,6 +14,8 @@ static NSString *const EXTRAPOLATE_TYPE_IDENTITY = @"identity"; static NSString *const EXTRAPOLATE_TYPE_CLAMP = @"clamp"; static NSString *const EXTRAPOLATE_TYPE_EXTEND = @"extend"; +RCT_EXTERN NSUInteger RCTFindIndexOfNearestValue(CGFloat value, NSArray *range); + RCT_EXTERN CGFloat RCTInterpolateValueInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange, diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.m b/Libraries/NativeAnimation/RCTAnimationUtils.m index 8f8d4cfb817a72..96697196a48fd3 100644 --- a/Libraries/NativeAnimation/RCTAnimationUtils.m +++ b/Libraries/NativeAnimation/RCTAnimationUtils.m @@ -9,7 +9,7 @@ #import -static NSUInteger _RCTFindIndexOfNearestValue(CGFloat value, NSArray *range) +NSUInteger RCTFindIndexOfNearestValue(CGFloat value, NSArray *range) { NSUInteger index; NSUInteger rangeCount = range.count; @@ -69,7 +69,7 @@ CGFloat RCTInterpolateValueInRange(CGFloat value, NSString *extrapolateLeft, NSString *extrapolateRight) { - NSUInteger rangeIndex = _RCTFindIndexOfNearestValue(value, inputRange); + NSUInteger rangeIndex = RCTFindIndexOfNearestValue(value, inputRange); CGFloat inputMin = inputRange[rangeIndex].doubleValue; CGFloat inputMax = inputRange[rangeIndex + 1].doubleValue; CGFloat outputMin = outputRange[rangeIndex].doubleValue; diff --git a/RNTester/js/NativeAnimationsExample.js b/RNTester/js/NativeAnimationsExample.js index 5a6e5df55e84e2..3f7caa9f260f86 100644 --- a/RNTester/js/NativeAnimationsExample.js +++ b/RNTester/js/NativeAnimationsExample.js @@ -314,6 +314,113 @@ class TrackingExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> } } +class InterpolatedExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + _value = 0 + _animationInterval: ?IntervalID; + constructor(props) { + super(props); + this.state = { + native: new Animated.Value(0), + nativeStart: new Animated.Value(0), + nativeEnd: new Animated.Value(200), + js: new Animated.Value(0), + jsStart: new Animated.Value(0), + jsEnd: new Animated.Value(200), + }; + this.state.jsInterpolated = this.state.js.interpolate({ + inputRange: [0, 1], + outputRange: [this.state.jsStart, this.state.jsEnd], + }); + this.state.nativeInterpolated = this.state.native.interpolate({ + inputRange: [0, 1], + outputRange: [this.state.nativeStart, this.state.nativeEnd], + }); + } + + animate = () => { + this._value = this._value === 0 ? 1 : 0; + Animated.spring( + this.state.native, + { + toValue: this._value, + useNativeDriver: true, + }, + ).start(); + Animated.spring( + this.state.js, + { + toValue: this._value, + }, + ).start(); + } + + changeBounds = (startAnim, startValue, endAnim, endValue, native) => { + Animated.parallel([ + Animated.spring( + startAnim, + { + toValue: startValue, + useNativeDriver: native, + }, + ), + Animated.spring( + endAnim, + { + toValue: endValue, + useNativeDriver: native, + }, + ), + ]).start(); + } + + componentDidMount() { + this._animationInterval = setInterval( + this.animate, + 1000 + ); + } + + componentWillUnmount() { + if (this._animationInterval) { + clearInterval(this._animationInterval); + } + } + + onPress = () => { + const start = Math.random() * 100; + const end = 100 + Math.random() * 100; + this.changeBounds(this.state.jsStart, start, this.state.jsEnd, end, false); + this.changeBounds(this.state.nativeStart, start, this.state.nativeEnd, end, true); + }; + + renderBlock = (animation, start, end) => [ + , + , + , + ] + + render() { + return ( + + + + Native: + + + {this.renderBlock(this.state.nativeInterpolated, this.state.nativeStart, this.state.nativeEnd)} + + + JavaScript: + + + {this.renderBlock(this.state.jsInterpolated, this.state.jsStart, this.state.jsEnd)} + + + + ); + } +} + const styles = StyleSheet.create({ row: { padding: 10, @@ -655,6 +762,12 @@ exports.examples = [ return ; }, }, + { + title: 'Interpolated with animated outputRange', + render: function() { + return ; + }, + }, { title: 'Internal Settings', render: function() { 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 b1a138e2b54f48..d74611f265b164 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java @@ -6,6 +6,7 @@ */ package com.facebook.react.animated; +import com.facebook.react.bridge.JSApplicationCausedNativeException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -22,6 +23,14 @@ public static final String EXTRAPOLATE_TYPE_CLAMP = "clamp"; public static final String EXTRAPOLATE_TYPE_EXTEND = "extend"; + private static int[] fromIntArray(ReadableArray ary) { + int[] res = new int[ary.size()]; + for (int i = 0; i < res.length; i++) { + res[i] = ary.getInt(i); + } + return res; + } + private static double[] fromDoubleArray(ReadableArray ary) { double[] res = new double[ary.size()]; for (int i = 0; i < res.length; i++) { @@ -75,22 +84,43 @@ private static double interpolate( (result - inputMin) / (inputMax - inputMin); } + private static ValueAnimatedNode getValueAnimatedNode( + NativeAnimatedNodesManager nativeAnimatedNodesManager, + int tag) { + AnimatedNode node = nativeAnimatedNodesManager.getNodeById(tag); + Boolean invalid = node == null || !(node instanceof ValueAnimatedNode); + if (invalid) { + String error = "Illegal node ID set in outputRange of Animated.interpolate node"; + throw new JSApplicationCausedNativeException(error); + } + return (ValueAnimatedNode) node; + } + /*package*/ static double interpolate( - double value, - double[] inputRange, - double[] outputRange, - String extrapolateLeft, - String extrapolateRight - ) { + NativeAnimatedNodesManager nativeAnimatedNodesManager, + double value, + double[] inputRange, + int[] outputRangeNodeTags, + String extrapolateLeft, + String extrapolateRight) { int rangeIndex = findRangeIndex(value, inputRange); - return interpolate( - value, - inputRange[rangeIndex], - inputRange[rangeIndex + 1], - outputRange[rangeIndex], - outputRange[rangeIndex + 1], - extrapolateLeft, - extrapolateRight); + double inputStart = inputRange[rangeIndex]; + double inputEnd = inputRange[rangeIndex + 1]; + double outputStart = InterpolationAnimatedNode.getValueAnimatedNode( + nativeAnimatedNodesManager, + outputRangeNodeTags[rangeIndex]).getValue(); + double outputEnd = InterpolationAnimatedNode.getValueAnimatedNode( + nativeAnimatedNodesManager, + outputRangeNodeTags[rangeIndex + 1]).getValue(); + return InterpolationAnimatedNode.interpolate( + value, + inputStart, + inputEnd, + outputStart, + outputEnd, + extrapolateLeft, + extrapolateRight + ); } private static int findRangeIndex(double value, double[] ranges) { @@ -103,38 +133,24 @@ private static int findRangeIndex(double value, double[] ranges) { return index - 1; } + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private @Nullable ValueAnimatedNode mParent; private final double mInputRange[]; - private final double mOutputRange[]; + private final int mOutputRangeNodeTags[]; private final String mExtrapolateLeft; private final String mExtrapolateRight; - private @Nullable ValueAnimatedNode mParent; - public InterpolationAnimatedNode(ReadableMap config) { + public InterpolationAnimatedNode( + ReadableMap config, + NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + mParent = (ValueAnimatedNode) nativeAnimatedNodesManager.getNodeById(config.getInt("parent")); mInputRange = fromDoubleArray(config.getArray("inputRange")); - mOutputRange = fromDoubleArray(config.getArray("outputRange")); + mOutputRangeNodeTags = fromIntArray(config.getArray("outputRange")); mExtrapolateLeft = config.getString("extrapolateLeft"); mExtrapolateRight = config.getString("extrapolateRight"); } - @Override - public void onAttachedToNode(AnimatedNode parent) { - if (mParent != null) { - throw new IllegalStateException("Parent already attached"); - } - if (!(parent instanceof ValueAnimatedNode)) { - throw new IllegalArgumentException("Parent is of an invalid type"); - } - mParent = (ValueAnimatedNode) parent; - } - - @Override - public void onDetachedFromNode(AnimatedNode parent) { - if (parent != mParent) { - throw new IllegalArgumentException("Invalid parent node provided"); - } - mParent = null; - } - @Override public void update() { if (mParent == null) { @@ -142,6 +158,13 @@ public void update() { // unattached node. return; } - mValue = interpolate(mParent.getValue(), mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight); + mValue = interpolate( + mNativeAnimatedNodesManager, + mParent.getValue(), + mInputRange, + mOutputRangeNodeTags, + mExtrapolateLeft, + mExtrapolateRight + ); } } 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 07357fd7eea998..c183f6a9e0e97e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -90,7 +90,7 @@ public void createAnimatedNode(int tag, ReadableMap config) { } else if ("props".equals(type)) { node = new PropsAnimatedNode(config, this, mUIImplementation); } else if ("interpolation".equals(type)) { - node = new InterpolationAnimatedNode(config); + node = new InterpolationAnimatedNode(config, this); } else if ("addition".equals(type)) { node = new AdditionAnimatedNode(config, this); } else if ("subtraction".equals(type)) { 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 27973a3acea1bf..5163ab5558c9af 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedInterpolationTest.java @@ -1,19 +1,119 @@ package com.facebook.react.animated; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.events.EventDispatcher; + +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; +import java.util.Map; + import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests method used by {@link InterpolationAnimatedNode} to interpolate value of the input nodes. */ +@PrepareForTest({Arguments.class}) @RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) public class NativeAnimatedInterpolationTest { + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private UIManagerModule mUIManagerMock; + private UIImplementation mUIImplementationMock; + private EventDispatcher mEventDispatcherMock; + private NativeAnimatedNodesManager mNativeAnimatedNodesManager; + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.class); + PowerMockito.when(Arguments.createArray()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyArray(); + } + }); + PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyMap(); + } + }); + + mUIManagerMock = mock(UIManagerModule.class); + mUIImplementationMock = mock(UIImplementation.class); + mEventDispatcherMock = mock(EventDispatcher.class); + PowerMockito.when(mUIManagerMock.getUIImplementation()).thenAnswer(new Answer() { + @Override + public UIImplementation answer(InvocationOnMock invocation) throws Throwable { + return mUIImplementationMock; + } + }); + PowerMockito.when(mUIManagerMock.getEventDispatcher()).thenAnswer(new Answer() { + @Override + public EventDispatcher answer(InvocationOnMock invocation) throws Throwable { + return mEventDispatcherMock; + } + }); + PowerMockito.when(mUIManagerMock.getConstants()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return MapBuilder.of("customDirectEventTypes", MapBuilder.newHashMap()); + } + }); + PowerMockito + .when(mUIManagerMock.getDirectEventNamesResolver()) + .thenAnswer(new Answer() { + @Override + public UIManagerModule.CustomEventNamesResolver answer(InvocationOnMock invocation) throws Throwable { + return new UIManagerModule.CustomEventNamesResolver() { + @Override + public String resolveCustomEventName(String eventName) { + Map directEventTypes = + (Map) mUIManagerMock.getConstants().get("customDirectEventTypes"); + if (directEventTypes != null) { + Map customEventType = (Map) directEventTypes.get(eventName); + if (customEventType != null) { + return customEventType.get("registrationName"); + } + } + return eventName; + } + }; + } + }); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); + } + + private int tag = 0; + + private int createNode(double value) { + tag = tag + 1; + mNativeAnimatedNodesManager.createAnimatedNode( + tag, + JavaOnlyMap.of("type", "value", "value", value, "offset", 0d)); + return tag; + } - private double simpleInterpolation(double value, double[] input, double[] output) { + private double simpleInterpolation(double value, double[] input, int[] output) { return InterpolationAnimatedNode.interpolate( + mNativeAnimatedNodesManager, value, input, output, @@ -25,7 +125,7 @@ private double simpleInterpolation(double value, double[] input, double[] output @Test public void testSimpleOneToOneMapping() { double[] input = new double[] {0d, 1d}; - double[] output = new double[] {0d, 1d}; + int[] output = new int[] {this.createNode(0d), this.createNode(1d)}; 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); @@ -35,7 +135,7 @@ public void testSimpleOneToOneMapping() { @Test public void testWiderOutputRange() { double[] input = new double[] {0d, 1d}; - double[] output = new double[] {100d, 200d}; + int[] output = new int[] {this.createNode(100d), this.createNode(200d)}; assertThat(simpleInterpolation(0, input, output)).isEqualTo(100); assertThat(simpleInterpolation(0.5, input, output)).isEqualTo(150); assertThat(simpleInterpolation(0.8, input, output)).isEqualTo(180); @@ -45,7 +145,7 @@ public void testWiderOutputRange() { @Test public void testWiderInputRange() { double[] input = new double[] {2000d, 3000d}; - double[] output = new double[] {1d, 2d}; + int[] output = new int[] {this.createNode(1d), this.createNode(2d)}; assertThat(simpleInterpolation(2000, input, output)).isEqualTo(1); assertThat(simpleInterpolation(2250, input, output)).isEqualTo(1.25); assertThat(simpleInterpolation(2800, input, output)).isEqualTo(1.8); @@ -55,7 +155,10 @@ public void testWiderInputRange() { @Test public void testManySegments() { double[] input = new double[] {-1d, 1d, 5d}; - double[] output = new double[] {0, 10d, 20d}; + int[] output = new int[] { + this.createNode(0d), + this.createNode(10d), + this.createNode(20d)}; assertThat(simpleInterpolation(-1, input, output)).isEqualTo(0); assertThat(simpleInterpolation(0, input, output)).isEqualTo(5); assertThat(simpleInterpolation(1, input, output)).isEqualTo(10); @@ -66,7 +169,7 @@ public void testManySegments() { @Test public void testExtendExtrapolate() { double[] input = new double[] {10d, 20d}; - double[] output = new double[] {0d, 1d}; + int[] output = new int[] {this.createNode(0d), this.createNode(1d)}; assertThat(simpleInterpolation(30d, input, output)).isEqualTo(2); assertThat(simpleInterpolation(5d, input, output)).isEqualTo(-0.5); } @@ -74,8 +177,9 @@ public void testExtendExtrapolate() { @Test public void testClampExtrapolate() { double[] input = new double[] {10d, 20d}; - double[] output = new double[] {0d, 1d}; + int[] output = new int[] {this.createNode(0d), this.createNode(1d)}; assertThat(InterpolationAnimatedNode.interpolate( + mNativeAnimatedNodesManager, 30d, input, output, @@ -83,6 +187,7 @@ public void testClampExtrapolate() { InterpolationAnimatedNode.EXTRAPOLATE_TYPE_CLAMP )).isEqualTo(1); assertThat(InterpolationAnimatedNode.interpolate( + mNativeAnimatedNodesManager, 5d, input, output, @@ -94,8 +199,9 @@ public void testClampExtrapolate() { @Test public void testIdentityExtrapolate() { double[] input = new double[] {10d, 20d}; - double[] output = new double[] {0d, 1d}; + int[] output = new int[] {this.createNode(0d), this.createNode(1d)}; assertThat(InterpolationAnimatedNode.interpolate( + mNativeAnimatedNodesManager, 30d, input, output, @@ -103,6 +209,7 @@ public void testIdentityExtrapolate() { InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY )).isEqualTo(30); assertThat(InterpolationAnimatedNode.interpolate( + mNativeAnimatedNodesManager, 5d, input, output, 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 3c31cb1cf00c12..7ae3d13e41937a 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -871,19 +871,32 @@ public void testHandleStoppingAnimation() { @Test public void testInterpolationNode() { + int parentNodeTag = 1; mNativeAnimatedNodesManager.createAnimatedNode( - 1, + parentNodeTag, JavaOnlyMap.of("type", "value", "value", 10d, "offset", 0d)); + int outputStartTag = 11; + mNativeAnimatedNodesManager.createAnimatedNode( + outputStartTag, + JavaOnlyMap.of("type", "value", "value", 0d, "offset", 0d)); + + int outputEndTag = 12; + mNativeAnimatedNodesManager.createAnimatedNode( + outputEndTag, + JavaOnlyMap.of("type", "value", "value", 1d, "offset", 0d)); + mNativeAnimatedNodesManager.createAnimatedNode( 2, JavaOnlyMap.of( + "parent", + parentNodeTag, "type", "interpolation", "inputRange", JavaOnlyArray.of(10d, 20d), "outputRange", - JavaOnlyArray.of(0d, 1d), + JavaOnlyArray.of(outputStartTag, outputEndTag), "extrapolateLeft", "extend", "extrapolateRight",