Skip to content

Commit

Permalink
Support Interpolation of strings when using native driver in Animated…
Browse files Browse the repository at this point in the history
…, fix Expected node to be marked as "native", optimize AnimatedNode creation and connections (#18187)

Summary:
Allow interpolation of strings with useNativeDriver. This allows animating much more of react-native-svg. This PR adds support for native animation of lengths with units, path data, colors etc. Plus, fixing the redundantly created nodes and (and thus, previously incorrect) connection of native animated nodes, improving performance.

Docs will need to change, specifying that string interpolation works with the native driver as well.

[GENERAL] [Added] Add support for native driven string interpolation in Animated
[GENERAL] Fix exception: Expected node to be marked as "native"
[GENERAL] Fix connection of AnimatedNodes and creation of redundant AnimatedNodes
Pull Request resolved: #18187

Differential Revision: D14597147

Pulled By: cpojer

fbshipit-source-id: 82a948a95419236be7931a8cc4ff72f41e477e9c
  • Loading branch information
msand authored and facebook-github-bot committed Mar 26, 2019
1 parent 81a702b commit 5e4a589
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 18 deletions.
3 changes: 1 addition & 2 deletions Libraries/Animated/src/NativeAnimatedHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,7 @@ function transformDataType(value: any): number {
const radians = (degrees * Math.PI) / 180.0;
return radians;
} else {
// Assume radians
return parseFloat(value) || 0;
return value;
}
}

Expand Down
1 change: 1 addition & 0 deletions Libraries/Animated/src/animations/Animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Animation {
}
__startNativeAnimation(animatedValue: AnimatedValue): void {
animatedValue.__makeNative();
animatedValue.__connectAnimatedNodes();
this.__nativeId = NativeAnimatedHelper.generateNewAnimationId();
NativeAnimatedHelper.API.startAnimatingNode(
this.__nativeId,
Expand Down
9 changes: 5 additions & 4 deletions Libraries/Animated/src/nodes/AnimatedInterpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,11 @@ function createInterpolationFromStringOutputRange(
// ->
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
return outputRange[0].replace(stringShapeRegex, () => {
const val = +interpolations[i++](input);
const rounded =
shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
return String(rounded);
let val = +interpolations[i++](input);
if (shouldRound) {
val = i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
}
return String(val);
});
};
}
Expand Down
8 changes: 7 additions & 1 deletion Libraries/Animated/src/nodes/AnimatedNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ class AnimatedNode {

/* Methods and props used by native Animated impl */
__isNative: boolean;
__isConnected: boolean;
__nativeTag: ?number;
__makeNative() {
if (!this.__isNative) {
throw new Error('This node cannot be made a "native" animated node');
}
}
__connectAnimatedNodes() {
if (!this.__isNative) {
throw new Error('This node cannot be connected natively');
}
}
__getNativeTag(): ?number {
NativeAnimatedHelper.assertNativeAnimatedModule();
invariant(
Expand All @@ -49,11 +55,11 @@ class AnimatedNode {
);
if (this.__nativeTag == null) {
const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag();
this.__nativeTag = nativeTag;
NativeAnimatedHelper.API.createAnimatedNode(
nativeTag,
this.__getNativeConfig(),
);
this.__nativeTag = nativeTag;
}
return this.__nativeTag;
}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Animated/src/nodes/AnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ class AnimatedProps extends AnimatedNode {
for (const propKey in this._props) {
const value = this._props[propKey];
if (value instanceof AnimatedNode) {
value.__makeNative();
propsConfig[propKey] = value.__getNativeTag();
}
}
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Animated/src/nodes/AnimatedStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ class AnimatedStyle extends AnimatedWithChildren {
const styleConfig = {};
for (const styleKey in this._style) {
if (this._style[styleKey] instanceof AnimatedNode) {
styleConfig[styleKey] = this._style[styleKey].__getNativeTag();
const style = this._style[styleKey];
style.__makeNative();
styleConfig[styleKey] = style.__getNativeTag();
}
// Non-animated styles are set using `setNativeProps`, no need
// to pass those as a part of the node config
Expand Down
9 changes: 9 additions & 0 deletions Libraries/Animated/src/nodes/AnimatedWithChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ class AnimatedWithChildren extends AnimatedNode {
this.__isNative = true;
for (const child of this._children) {
child.__makeNative();
}
}
}

__connectAnimatedNodes() {
if (!this.__isConnected) {
this.__isConnected = true;
for (const child of this._children) {
child.__connectAnimatedNodes();
NativeAnimatedHelper.API.connectAnimatedNodes(
this.__getNativeTag(),
child.__getNativeTag(),
Expand Down
108 changes: 103 additions & 5 deletions Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,89 @@

#import "RCTAnimationUtils.h"

static NSRegularExpression *regex;

@implementation RCTInterpolationAnimatedNode
{
__weak RCTValueAnimatedNode *_parentNode;
NSArray<NSNumber *> *_inputRange;
NSArray<NSNumber *> *_outputRange;
NSArray<NSArray<NSNumber *> *> *_outputs;
NSArray<NSString *> *_soutputRange;
NSString *_extrapolateLeft;
NSString *_extrapolateRight;
NSUInteger _numVals;
bool _hasStringOutput;
bool _shouldRound;
NSArray<NSTextCheckingResult*> *_matches;
}

- (instancetype)initWithTag:(NSNumber *)tag
config:(NSDictionary<NSString *, id> *)config
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
regex = [NSRegularExpression regularExpressionWithPattern:@"[0-9.-]+" options:NSRegularExpressionCaseInsensitive error:nil];
});
if ((self = [super initWithTag:tag config:config])) {
_inputRange = [config[@"inputRange"] copy];
NSMutableArray *outputRange = [NSMutableArray array];
NSMutableArray *soutputRange = [NSMutableArray array];
NSMutableArray<NSMutableArray<NSNumber *> *> *_outputRanges = [NSMutableArray array];

_hasStringOutput = NO;
for (id value in config[@"outputRange"]) {
if ([value isKindOfClass:[NSNumber class]]) {
[outputRange addObject:value];
} else if ([value isKindOfClass:[NSString class]]) {
/**
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
NSMutableArray *output = [NSMutableArray array];
[_outputRanges addObject:output];
[soutputRange addObject:value];

_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
for (NSTextCheckingResult *match in _matches) {
NSString* strNumber = [value substringWithRange:match.range];
[output addObject:[NSNumber numberWithDouble:strNumber.doubleValue]];
}

_hasStringOutput = YES;
[outputRange addObject:[output objectAtIndex:0]];
}
}
if (_hasStringOutput) {
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
// ->
// [
// [0, 50],
// [100, 150],
// [200, 250],
// [0, 0.5],
// ]
_numVals = [_matches count];
NSString *value = [soutputRange objectAtIndex:0];
_shouldRound = [value containsString:@"rgb"];
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
NSMutableArray<NSMutableArray<NSNumber *> *> *outputs = [NSMutableArray arrayWithCapacity:_numVals];
NSUInteger size = [soutputRange count];
for (NSUInteger j = 0; j < _numVals; j++) {
NSMutableArray *output = [NSMutableArray arrayWithCapacity:size];
[outputs addObject:output];
for (int i = 0; i < size; i++) {
[output addObject:[[_outputRanges objectAtIndex:i] objectAtIndex:j]];
}
}
_outputs = [outputs copy];
}
_outputRange = [outputRange copy];
_soutputRange = [soutputRange copy];
_extrapolateLeft = config[@"extrapolateLeft"];
_extrapolateRight = config[@"extrapolateRight"];
}
Expand Down Expand Up @@ -61,11 +123,47 @@ - (void)performUpdate

CGFloat inputValue = _parentNode.value;

self.value = RCTInterpolateValueInRange(inputValue,
_inputRange,
_outputRange,
_extrapolateLeft,
_extrapolateRight);
CGFloat interpolated = RCTInterpolateValueInRange(inputValue,
_inputRange,
_outputRange,
_extrapolateLeft,
_extrapolateRight);
self.value = interpolated;
if (_hasStringOutput) {
// 'rgba(0, 100, 200, 0)'
// ->
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
if (_numVals > 1) {
NSString *text = _soutputRange[0];
NSMutableString *formattedText = [NSMutableString stringWithString:text];
NSUInteger i = _numVals;
for (NSTextCheckingResult *match in [_matches reverseObjectEnumerator]) {
CGFloat val = RCTInterpolateValueInRange(inputValue,
_inputRange,
_outputs[--i],
_extrapolateLeft,
_extrapolateRight);
NSString *str;
if (_shouldRound) {
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
// round the opacity (4th column).
bool isAlpha = i == 3;
CGFloat rounded = isAlpha ? round(val * 1000) / 1000 : round(val);
str = isAlpha ? [NSString stringWithFormat:@"%1.3f", rounded] : [NSString stringWithFormat:@"%1.0f", rounded];
} else {
str = [NSString stringWithFormat:@"%1f", val];
}

[formattedText replaceCharactersInRange:[match range] withString:str];
}
self.animatedObject = formattedText;
} else {
self.animatedObject = [regex stringByReplacingMatchesInString:_soutputRange[0]
options:0
range:NSMakeRange(0, _soutputRange[0].length)
withTemplate:[NSString stringWithFormat:@"%1f", interpolated]];
}
}
}

@end
9 changes: 7 additions & 2 deletions Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,13 @@ - (void)performUpdate

} else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) {
NSString *property = [self propertyNameForParentTag:parentTag];
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
self->_propsDictionary[property] = @(value);
id animatedObject = [(RCTValueAnimatedNode *)parentNode animatedObject];
if (animatedObject) {
self->_propsDictionary[property] = animatedObject;
} else {
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
self->_propsDictionary[property] = @(value);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- (void)extractOffset;

@property (nonatomic, assign) CGFloat value;
@property (nonatomic, strong) id animatedObject;
@property (nonatomic, weak) id<RCTValueAnimatedNodeObserver> valueObserver;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

/**
Expand All @@ -21,6 +27,7 @@
public static final String EXTRAPOLATE_TYPE_IDENTITY = "identity";
public static final String EXTRAPOLATE_TYPE_CLAMP = "clamp";
public static final String EXTRAPOLATE_TYPE_EXTEND = "extend";
static final Pattern regex = Pattern.compile("[0-9.-]+");

private static double[] fromDoubleArray(ReadableArray ary) {
double[] res = new double[ary.size()];
Expand Down Expand Up @@ -105,13 +112,68 @@ private static int findRangeIndex(double value, double[] ranges) {

private final double mInputRange[];
private final double mOutputRange[];
private String mPattern;
private double mOutputs[][];
private final boolean mHasStringOutput;
private final Matcher mSOutputMatcher;
private final String mExtrapolateLeft;
private final String mExtrapolateRight;
private @Nullable ValueAnimatedNode mParent;
private boolean mShouldRound;
private int mNumVals;

public InterpolationAnimatedNode(ReadableMap config) {
mInputRange = fromDoubleArray(config.getArray("inputRange"));
mOutputRange = fromDoubleArray(config.getArray("outputRange"));
ReadableArray output = config.getArray("outputRange");
mHasStringOutput = output.getType(0) == ReadableType.String;
if (mHasStringOutput) {
/*
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
int size = output.size();
mOutputRange = new double[size];
mPattern = output.getString(0);
mShouldRound = mPattern.startsWith("rgb");
mSOutputMatcher = regex.matcher(mPattern);
ArrayList<ArrayList<Double>> mOutputRanges = new ArrayList<>();
for (int i = 0; i < size; i++) {
String val = output.getString(i);
Matcher m = regex.matcher(val);
ArrayList<Double> outputRange = new ArrayList<>();
mOutputRanges.add(outputRange);
while (m.find()) {
Double parsed = Double.parseDouble(m.group());
outputRange.add(parsed);
}
mOutputRange[i] = outputRange.get(0);
}

// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
// ->
// [
// [0, 50],
// [100, 150],
// [200, 250],
// [0, 0.5],
// ]
mNumVals = mOutputRanges.get(0).size();
mOutputs = new double[mNumVals][];
for (int j = 0; j < mNumVals; j++) {
double[] arr = new double[size];
mOutputs[j] = arr;
for (int i = 0; i < size; i++) {
arr[i] = mOutputRanges.get(i).get(j);
}
}
} else {
mOutputRange = fromDoubleArray(output);
mSOutputMatcher = null;
}
mExtrapolateLeft = config.getString("extrapolateLeft");
mExtrapolateRight = config.getString("extrapolateRight");
}
Expand Down Expand Up @@ -142,6 +204,33 @@ public void update() {
// unattached node.
return;
}
mValue = interpolate(mParent.getValue(), mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
double value = mParent.getValue();
mValue = interpolate(value, mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
if (mHasStringOutput) {
// 'rgba(0, 100, 200, 0)'
// ->
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
if (mNumVals > 1) {
StringBuffer sb = new StringBuffer(mPattern.length());
int i = 0;
mSOutputMatcher.reset();
while (mSOutputMatcher.find()) {
double val = interpolate(value, mInputRange, mOutputs[i++], mExtrapolateLeft, mExtrapolateRight);
if (mShouldRound) {
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
// round the opacity (4th column).
boolean isAlpha = i == 4;
int rounded = (int)Math.round(isAlpha ? val * 1000 : val);
mSOutputMatcher.appendReplacement(sb, isAlpha ? String.valueOf((double)rounded / 1000) : String.valueOf(rounded));
} else {
mSOutputMatcher.appendReplacement(sb, String.valueOf(val));
}
}
mSOutputMatcher.appendTail(sb);
mAnimatedObject = sb.toString();
} else {
mAnimatedObject = mSOutputMatcher.replaceFirst(String.valueOf(mValue));
}
}
}
}
Loading

0 comments on commit 5e4a589

Please sign in to comment.