Skip to content

Commit bdc530b

Browse files
msandfacebook-github-bot
authored andcommitted
Fix connection of animated nodes and scroll offset with useNativeDriver. (#24177)
Summary: Add example showing regression before this fix is applied. #18187 Was found to introduce a regression in some internal facebook code-base end to end test which couldn't be shared. I was able to create a reproducible demo of a regression I found, and made a fix for it. Hopefully this will fix the internal test, such that the pr can stay merged. ## Changelog [GENERAL] [Fixed] - Fix connection of animated nodes and scroll offset with useNativeDriver. Pull Request resolved: #24177 Reviewed By: rickhanlonii Differential Revision: D14845617 Pulled By: cpojer fbshipit-source-id: 1f121dbe773b0cde2adf1ee5a8c3c0266034e50d
1 parent 417e191 commit bdc530b

15 files changed

+366
-21
lines changed

Libraries/Animated/src/NativeAnimatedHelper.js

+21-4
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,25 @@ let __nativeAnimationIdCount = 1; /* used for started animations */
2727

2828
let nativeEventEmitter;
2929

30+
let queueConnections = false;
31+
let queue = [];
32+
3033
/**
3134
* Simple wrappers around NativeAnimatedModule to provide flow and autocmplete support for
3235
* the native module methods
3336
*/
3437
const API = {
38+
enableQueue: function(): void {
39+
queueConnections = true;
40+
},
41+
disableQueue: function(): void {
42+
invariant(NativeAnimatedModule, 'Native animated module is not available');
43+
queueConnections = false;
44+
while (queue.length) {
45+
const args = queue.shift();
46+
NativeAnimatedModule.connectAnimatedNodes(args[0], args[1]);
47+
}
48+
},
3549
createAnimatedNode: function(tag: ?number, config: AnimatedNodeConfig): void {
3650
invariant(NativeAnimatedModule, 'Native animated module is not available');
3751
NativeAnimatedModule.createAnimatedNode(tag, config);
@@ -46,6 +60,10 @@ const API = {
4660
},
4761
connectAnimatedNodes: function(parentTag: ?number, childTag: ?number): void {
4862
invariant(NativeAnimatedModule, 'Native animated module is not available');
63+
if (queueConnections) {
64+
queue.push([parentTag, childTag]);
65+
return;
66+
}
4967
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag);
5068
},
5169
disconnectAnimatedNodes: function(
@@ -197,7 +215,7 @@ function addWhitelistedInterpolationParam(param: string): void {
197215
function validateTransform(
198216
configs: Array<
199217
| {type: 'animated', property: string, nodeTag: ?number}
200-
| {type: 'static', property: string, value: number},
218+
| {type: 'static', property: string, value: number | string},
201219
>,
202220
): void {
203221
configs.forEach(config => {
@@ -263,7 +281,7 @@ function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean {
263281
return config.useNativeDriver || false;
264282
}
265283

266-
function transformDataType(value: number | string): number {
284+
function transformDataType(value: number | string): number | string {
267285
// Change the string type to number type so we can reuse the same logic in
268286
// iOS and Android platform
269287
if (typeof value !== 'string') {
@@ -274,8 +292,7 @@ function transformDataType(value: number | string): number {
274292
const radians = (degrees * Math.PI) / 180.0;
275293
return radians;
276294
} else {
277-
// Assume radians
278-
return parseFloat(value) || 0;
295+
return value;
279296
}
280297
}
281298

Libraries/Animated/src/animations/Animation.js

+2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ class Animation {
5656
onEnd && onEnd(result);
5757
}
5858
__startNativeAnimation(animatedValue: AnimatedValue): void {
59+
NativeAnimatedHelper.API.enableQueue();
5960
animatedValue.__makeNative();
61+
NativeAnimatedHelper.API.disableQueue();
6062
this.__nativeId = NativeAnimatedHelper.generateNewAnimationId();
6163
NativeAnimatedHelper.API.startAnimatingNode(
6264
this.__nativeId,

Libraries/Animated/src/nodes/AnimatedInterpolation.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ function colorToRgba(input: string): string {
181181
return `rgba(${r}, ${g}, ${b}, ${a})`;
182182
}
183183

184-
const stringShapeRegex = /[0-9\.-]+/g;
184+
const stringShapeRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
185185

186186
/**
187187
* Supports string shapes by extracting numbers so new values can be computed,
@@ -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

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ class AnimatedNode {
157157
);
158158
if (this.__nativeTag == null) {
159159
const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag();
160+
this.__nativeTag = nativeTag;
160161
NativeAnimatedHelper.API.createAnimatedNode(
161162
nativeTag,
162163
this.__getNativeConfig(),
163164
);
164-
this.__nativeTag = nativeTag;
165165
this.__shouldUpdateListenersForNewNativeTag = true;
166166
}
167167
return this.__nativeTag;

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/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m

+105-5
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,90 @@
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+
NSString *fpRegex = @"[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?";
35+
regex = [NSRegularExpression regularExpressionWithPattern:fpRegex options:NSRegularExpressionCaseInsensitive error:nil];
36+
});
2437
if ((self = [super initWithTag:tag config:config])) {
2538
_inputRange = [config[@"inputRange"] copy];
2639
NSMutableArray *outputRange = [NSMutableArray array];
40+
NSMutableArray *soutputRange = [NSMutableArray array];
41+
NSMutableArray<NSMutableArray<NSNumber *> *> *_outputRanges = [NSMutableArray array];
42+
43+
_hasStringOutput = NO;
2744
for (id value in config[@"outputRange"]) {
2845
if ([value isKindOfClass:[NSNumber class]]) {
2946
[outputRange addObject:value];
47+
} else if ([value isKindOfClass:[NSString class]]) {
48+
/**
49+
* Supports string shapes by extracting numbers so new values can be computed,
50+
* and recombines those values into new strings of the same shape. Supports
51+
* things like:
52+
*
53+
* rgba(123, 42, 99, 0.36) // colors
54+
* -45deg // values with units
55+
*/
56+
NSMutableArray *output = [NSMutableArray array];
57+
[_outputRanges addObject:output];
58+
[soutputRange addObject:value];
59+
60+
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
61+
for (NSTextCheckingResult *match in _matches) {
62+
NSString* strNumber = [value substringWithRange:match.range];
63+
[output addObject:[NSNumber numberWithDouble:strNumber.doubleValue]];
64+
}
65+
66+
_hasStringOutput = YES;
67+
[outputRange addObject:[output objectAtIndex:0]];
68+
}
69+
}
70+
if (_hasStringOutput) {
71+
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
72+
// ->
73+
// [
74+
// [0, 50],
75+
// [100, 150],
76+
// [200, 250],
77+
// [0, 0.5],
78+
// ]
79+
_numVals = [_matches count];
80+
NSString *value = [soutputRange objectAtIndex:0];
81+
_shouldRound = [value containsString:@"rgb"];
82+
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
83+
NSMutableArray<NSMutableArray<NSNumber *> *> *outputs = [NSMutableArray arrayWithCapacity:_numVals];
84+
NSUInteger size = [soutputRange count];
85+
for (NSUInteger j = 0; j < _numVals; j++) {
86+
NSMutableArray *output = [NSMutableArray arrayWithCapacity:size];
87+
[outputs addObject:output];
88+
for (int i = 0; i < size; i++) {
89+
[output addObject:[[_outputRanges objectAtIndex:i] objectAtIndex:j]];
90+
}
3091
}
92+
_outputs = [outputs copy];
3193
}
3294
_outputRange = [outputRange copy];
95+
_soutputRange = [soutputRange copy];
3396
_extrapolateLeft = config[@"extrapolateLeft"];
3497
_extrapolateRight = config[@"extrapolateRight"];
3598
}
@@ -61,11 +124,48 @@ - (void)performUpdate
61124

62125
CGFloat inputValue = _parentNode.value;
63126

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

71171
@end

Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m

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

116116
} else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) {
117117
NSString *property = [self propertyNameForParentTag:parentTag];
118-
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
119-
self->_propsDictionary[property] = @(value);
118+
id animatedObject = [(RCTValueAnimatedNode *)parentNode animatedObject];
119+
if (animatedObject) {
120+
self->_propsDictionary[property] = animatedObject;
121+
} else {
122+
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
123+
self->_propsDictionary[property] = @(value);
124+
}
120125
}
121126
}
122127

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

0 commit comments

Comments
 (0)