Skip to content

Commit 5e4a589

Browse files
msandfacebook-github-bot
authored andcommitted
Support Interpolation of strings when using native driver in Animated, 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
1 parent 81a702b commit 5e4a589

13 files changed

+240
-18
lines changed

Libraries/Animated/src/NativeAnimatedHelper.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,7 @@ function transformDataType(value: any): number {
271271
const radians = (degrees * Math.PI) / 180.0;
272272
return radians;
273273
} else {
274-
// Assume radians
275-
return parseFloat(value) || 0;
274+
return value;
276275
}
277276
}
278277

Libraries/Animated/src/animations/Animation.js

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Animation {
5757
}
5858
__startNativeAnimation(animatedValue: AnimatedValue): void {
5959
animatedValue.__makeNative();
60+
animatedValue.__connectAnimatedNodes();
6061
this.__nativeId = NativeAnimatedHelper.generateNewAnimationId();
6162
NativeAnimatedHelper.API.startAnimatingNode(
6263
this.__nativeId,

Libraries/Animated/src/nodes/AnimatedInterpolation.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,11 @@ function createInterpolationFromStringOutputRange(
242242
// ->
243243
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
244244
return outputRange[0].replace(stringShapeRegex, () => {
245-
const val = +interpolations[i++](input);
246-
const rounded =
247-
shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
248-
return String(rounded);
245+
let val = +interpolations[i++](input);
246+
if (shouldRound) {
247+
val = i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
248+
}
249+
return String(val);
249250
});
250251
};
251252
}

Libraries/Animated/src/nodes/AnimatedNode.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ class AnimatedNode {
3535

3636
/* Methods and props used by native Animated impl */
3737
__isNative: boolean;
38+
__isConnected: boolean;
3839
__nativeTag: ?number;
3940
__makeNative() {
4041
if (!this.__isNative) {
4142
throw new Error('This node cannot be made a "native" animated node');
4243
}
4344
}
45+
__connectAnimatedNodes() {
46+
if (!this.__isNative) {
47+
throw new Error('This node cannot be connected natively');
48+
}
49+
}
4450
__getNativeTag(): ?number {
4551
NativeAnimatedHelper.assertNativeAnimatedModule();
4652
invariant(
@@ -49,11 +55,11 @@ class AnimatedNode {
4955
);
5056
if (this.__nativeTag == null) {
5157
const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag();
58+
this.__nativeTag = nativeTag;
5259
NativeAnimatedHelper.API.createAnimatedNode(
5360
nativeTag,
5461
this.__getNativeConfig(),
5562
);
56-
this.__nativeTag = nativeTag;
5763
}
5864
return this.__nativeTag;
5965
}

Libraries/Animated/src/nodes/AnimatedProps.js

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class AnimatedProps extends AnimatedNode {
151151
for (const propKey in this._props) {
152152
const value = this._props[propKey];
153153
if (value instanceof AnimatedNode) {
154+
value.__makeNative();
154155
propsConfig[propKey] = value.__getNativeTag();
155156
}
156157
}

Libraries/Animated/src/nodes/AnimatedStyle.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ class AnimatedStyle extends AnimatedWithChildren {
108108
const styleConfig = {};
109109
for (const styleKey in this._style) {
110110
if (this._style[styleKey] instanceof AnimatedNode) {
111-
styleConfig[styleKey] = this._style[styleKey].__getNativeTag();
111+
const style = this._style[styleKey];
112+
style.__makeNative();
113+
styleConfig[styleKey] = style.__getNativeTag();
112114
}
113115
// Non-animated styles are set using `setNativeProps`, no need
114116
// to pass those as a part of the node config

Libraries/Animated/src/nodes/AnimatedWithChildren.js

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ class AnimatedWithChildren extends AnimatedNode {
2525
this.__isNative = true;
2626
for (const child of this._children) {
2727
child.__makeNative();
28+
}
29+
}
30+
}
31+
32+
__connectAnimatedNodes() {
33+
if (!this.__isConnected) {
34+
this.__isConnected = true;
35+
for (const child of this._children) {
36+
child.__connectAnimatedNodes();
2837
NativeAnimatedHelper.API.connectAnimatedNodes(
2938
this.__getNativeTag(),
3039
child.__getNativeTag(),

Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m

+103-5
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,89 @@
99

1010
#import "RCTAnimationUtils.h"
1111

12+
static NSRegularExpression *regex;
13+
1214
@implementation RCTInterpolationAnimatedNode
1315
{
1416
__weak RCTValueAnimatedNode *_parentNode;
1517
NSArray<NSNumber *> *_inputRange;
1618
NSArray<NSNumber *> *_outputRange;
19+
NSArray<NSArray<NSNumber *> *> *_outputs;
20+
NSArray<NSString *> *_soutputRange;
1721
NSString *_extrapolateLeft;
1822
NSString *_extrapolateRight;
23+
NSUInteger _numVals;
24+
bool _hasStringOutput;
25+
bool _shouldRound;
26+
NSArray<NSTextCheckingResult*> *_matches;
1927
}
2028

2129
- (instancetype)initWithTag:(NSNumber *)tag
2230
config:(NSDictionary<NSString *, id> *)config
2331
{
32+
static dispatch_once_t onceToken;
33+
dispatch_once(&onceToken, ^{
34+
regex = [NSRegularExpression regularExpressionWithPattern:@"[0-9.-]+" options:NSRegularExpressionCaseInsensitive error:nil];
35+
});
2436
if ((self = [super initWithTag:tag config:config])) {
2537
_inputRange = [config[@"inputRange"] copy];
2638
NSMutableArray *outputRange = [NSMutableArray array];
39+
NSMutableArray *soutputRange = [NSMutableArray array];
40+
NSMutableArray<NSMutableArray<NSNumber *> *> *_outputRanges = [NSMutableArray array];
41+
42+
_hasStringOutput = NO;
2743
for (id value in config[@"outputRange"]) {
2844
if ([value isKindOfClass:[NSNumber class]]) {
2945
[outputRange addObject:value];
46+
} else if ([value isKindOfClass:[NSString class]]) {
47+
/**
48+
* Supports string shapes by extracting numbers so new values can be computed,
49+
* and recombines those values into new strings of the same shape. Supports
50+
* things like:
51+
*
52+
* rgba(123, 42, 99, 0.36) // colors
53+
* -45deg // values with units
54+
*/
55+
NSMutableArray *output = [NSMutableArray array];
56+
[_outputRanges addObject:output];
57+
[soutputRange addObject:value];
58+
59+
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
60+
for (NSTextCheckingResult *match in _matches) {
61+
NSString* strNumber = [value substringWithRange:match.range];
62+
[output addObject:[NSNumber numberWithDouble:strNumber.doubleValue]];
63+
}
64+
65+
_hasStringOutput = YES;
66+
[outputRange addObject:[output objectAtIndex:0]];
67+
}
68+
}
69+
if (_hasStringOutput) {
70+
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
71+
// ->
72+
// [
73+
// [0, 50],
74+
// [100, 150],
75+
// [200, 250],
76+
// [0, 0.5],
77+
// ]
78+
_numVals = [_matches count];
79+
NSString *value = [soutputRange objectAtIndex:0];
80+
_shouldRound = [value containsString:@"rgb"];
81+
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
82+
NSMutableArray<NSMutableArray<NSNumber *> *> *outputs = [NSMutableArray arrayWithCapacity:_numVals];
83+
NSUInteger size = [soutputRange count];
84+
for (NSUInteger j = 0; j < _numVals; j++) {
85+
NSMutableArray *output = [NSMutableArray arrayWithCapacity:size];
86+
[outputs addObject:output];
87+
for (int i = 0; i < size; i++) {
88+
[output addObject:[[_outputRanges objectAtIndex:i] objectAtIndex:j]];
89+
}
3090
}
91+
_outputs = [outputs copy];
3192
}
3293
_outputRange = [outputRange copy];
94+
_soutputRange = [soutputRange copy];
3395
_extrapolateLeft = config[@"extrapolateLeft"];
3496
_extrapolateRight = config[@"extrapolateRight"];
3597
}
@@ -61,11 +123,47 @@ - (void)performUpdate
61123

62124
CGFloat inputValue = _parentNode.value;
63125

64-
self.value = RCTInterpolateValueInRange(inputValue,
65-
_inputRange,
66-
_outputRange,
67-
_extrapolateLeft,
68-
_extrapolateRight);
126+
CGFloat interpolated = RCTInterpolateValueInRange(inputValue,
127+
_inputRange,
128+
_outputRange,
129+
_extrapolateLeft,
130+
_extrapolateRight);
131+
self.value = interpolated;
132+
if (_hasStringOutput) {
133+
// 'rgba(0, 100, 200, 0)'
134+
// ->
135+
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
136+
if (_numVals > 1) {
137+
NSString *text = _soutputRange[0];
138+
NSMutableString *formattedText = [NSMutableString stringWithString:text];
139+
NSUInteger i = _numVals;
140+
for (NSTextCheckingResult *match in [_matches reverseObjectEnumerator]) {
141+
CGFloat val = RCTInterpolateValueInRange(inputValue,
142+
_inputRange,
143+
_outputs[--i],
144+
_extrapolateLeft,
145+
_extrapolateRight);
146+
NSString *str;
147+
if (_shouldRound) {
148+
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
149+
// round the opacity (4th column).
150+
bool isAlpha = i == 3;
151+
CGFloat rounded = isAlpha ? round(val * 1000) / 1000 : round(val);
152+
str = isAlpha ? [NSString stringWithFormat:@"%1.3f", rounded] : [NSString stringWithFormat:@"%1.0f", rounded];
153+
} else {
154+
str = [NSString stringWithFormat:@"%1f", val];
155+
}
156+
157+
[formattedText replaceCharactersInRange:[match range] withString:str];
158+
}
159+
self.animatedObject = formattedText;
160+
} else {
161+
self.animatedObject = [regex stringByReplacingMatchesInString:_soutputRange[0]
162+
options:0
163+
range:NSMakeRange(0, _soutputRange[0].length)
164+
withTemplate:[NSString stringWithFormat:@"%1f", interpolated]];
165+
}
166+
}
69167
}
70168

71169
@end

Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m

+7-2
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,13 @@ - (void)performUpdate
110110

111111
} else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) {
112112
NSString *property = [self propertyNameForParentTag:parentTag];
113-
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
114-
self->_propsDictionary[property] = @(value);
113+
id animatedObject = [(RCTValueAnimatedNode *)parentNode animatedObject];
114+
if (animatedObject) {
115+
self->_propsDictionary[property] = animatedObject;
116+
} else {
117+
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
118+
self->_propsDictionary[property] = @(value);
119+
}
115120
}
116121
}
117122

Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- (void)extractOffset;
2525

2626
@property (nonatomic, assign) CGFloat value;
27+
@property (nonatomic, strong) id animatedObject;
2728
@property (nonatomic, weak) id<RCTValueAnimatedNodeObserver> valueObserver;
2829

2930
@end

ReactAndroid/src/main/java/com/facebook/react/animated/InterpolationAnimatedNode.java

+91-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
1010
import com.facebook.react.bridge.ReadableArray;
1111
import com.facebook.react.bridge.ReadableMap;
12+
import com.facebook.react.bridge.ReadableType;
13+
14+
import java.util.ArrayList;
15+
import java.util.regex.Matcher;
16+
import java.util.regex.Pattern;
17+
1218
import javax.annotation.Nullable;
1319

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

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

106113
private final double mInputRange[];
107114
private final double mOutputRange[];
115+
private String mPattern;
116+
private double mOutputs[][];
117+
private final boolean mHasStringOutput;
118+
private final Matcher mSOutputMatcher;
108119
private final String mExtrapolateLeft;
109120
private final String mExtrapolateRight;
110121
private @Nullable ValueAnimatedNode mParent;
122+
private boolean mShouldRound;
123+
private int mNumVals;
111124

112125
public InterpolationAnimatedNode(ReadableMap config) {
113126
mInputRange = fromDoubleArray(config.getArray("inputRange"));
114-
mOutputRange = fromDoubleArray(config.getArray("outputRange"));
127+
ReadableArray output = config.getArray("outputRange");
128+
mHasStringOutput = output.getType(0) == ReadableType.String;
129+
if (mHasStringOutput) {
130+
/*
131+
* Supports string shapes by extracting numbers so new values can be computed,
132+
* and recombines those values into new strings of the same shape. Supports
133+
* things like:
134+
*
135+
* rgba(123, 42, 99, 0.36) // colors
136+
* -45deg // values with units
137+
*/
138+
int size = output.size();
139+
mOutputRange = new double[size];
140+
mPattern = output.getString(0);
141+
mShouldRound = mPattern.startsWith("rgb");
142+
mSOutputMatcher = regex.matcher(mPattern);
143+
ArrayList<ArrayList<Double>> mOutputRanges = new ArrayList<>();
144+
for (int i = 0; i < size; i++) {
145+
String val = output.getString(i);
146+
Matcher m = regex.matcher(val);
147+
ArrayList<Double> outputRange = new ArrayList<>();
148+
mOutputRanges.add(outputRange);
149+
while (m.find()) {
150+
Double parsed = Double.parseDouble(m.group());
151+
outputRange.add(parsed);
152+
}
153+
mOutputRange[i] = outputRange.get(0);
154+
}
155+
156+
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
157+
// ->
158+
// [
159+
// [0, 50],
160+
// [100, 150],
161+
// [200, 250],
162+
// [0, 0.5],
163+
// ]
164+
mNumVals = mOutputRanges.get(0).size();
165+
mOutputs = new double[mNumVals][];
166+
for (int j = 0; j < mNumVals; j++) {
167+
double[] arr = new double[size];
168+
mOutputs[j] = arr;
169+
for (int i = 0; i < size; i++) {
170+
arr[i] = mOutputRanges.get(i).get(j);
171+
}
172+
}
173+
} else {
174+
mOutputRange = fromDoubleArray(output);
175+
mSOutputMatcher = null;
176+
}
115177
mExtrapolateLeft = config.getString("extrapolateLeft");
116178
mExtrapolateRight = config.getString("extrapolateRight");
117179
}
@@ -142,6 +204,33 @@ public void update() {
142204
// unattached node.
143205
return;
144206
}
145-
mValue = interpolate(mParent.getValue(), mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
207+
double value = mParent.getValue();
208+
mValue = interpolate(value, mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
209+
if (mHasStringOutput) {
210+
// 'rgba(0, 100, 200, 0)'
211+
// ->
212+
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
213+
if (mNumVals > 1) {
214+
StringBuffer sb = new StringBuffer(mPattern.length());
215+
int i = 0;
216+
mSOutputMatcher.reset();
217+
while (mSOutputMatcher.find()) {
218+
double val = interpolate(value, mInputRange, mOutputs[i++], mExtrapolateLeft, mExtrapolateRight);
219+
if (mShouldRound) {
220+
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
221+
// round the opacity (4th column).
222+
boolean isAlpha = i == 4;
223+
int rounded = (int)Math.round(isAlpha ? val * 1000 : val);
224+
mSOutputMatcher.appendReplacement(sb, isAlpha ? String.valueOf((double)rounded / 1000) : String.valueOf(rounded));
225+
} else {
226+
mSOutputMatcher.appendReplacement(sb, String.valueOf(val));
227+
}
228+
}
229+
mSOutputMatcher.appendTail(sb);
230+
mAnimatedObject = sb.toString();
231+
} else {
232+
mAnimatedObject = mSOutputMatcher.replaceFirst(String.valueOf(mValue));
233+
}
234+
}
146235
}
147236
}

0 commit comments

Comments
 (0)