From 6003e70e84c369d7dc2c6bea50ea41f0bac79595 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 30 Nov 2022 10:19:12 -0800 Subject: [PATCH] Support colors for AnimatedInterpolation on iOS Summary: Color support for AnimatedInterpolation was incomplete with native drivers, as only rgba type strings were supported. There was also an issue where color props instead a StyleAnimatedNode would never get applied. We were also potentially duplicating color parsing support, which is already centralized in normalizeColor / processColor. Changelog: [iOS][Added] Enable AnimatedInterpolation to interpolate arbitrary color types. Reviewed By: sammy-SC Differential Revision: D40632443 fbshipit-source-id: 4dfb29edca4b919474408b43c3917ac9406a147a --- .../Nodes/RCTColorAnimatedNode.m | 5 +- .../Nodes/RCTInterpolationAnimatedNode.h | 10 + .../Nodes/RCTInterpolationAnimatedNode.m | 210 +++++++++--------- .../Nodes/RCTStyleAnimatedNode.m | 7 +- .../Nodes/RCTValueAnimatedNode.h | 2 +- .../Nodes/RCTValueAnimatedNode.m | 5 + Libraries/NativeAnimation/RCTAnimationUtils.h | 27 ++- Libraries/NativeAnimation/RCTAnimationUtils.m | 30 ++- .../RCTAnimationUtilsTests.m | 32 +++ 9 files changed, 205 insertions(+), 123 deletions(-) diff --git a/Libraries/NativeAnimation/Nodes/RCTColorAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTColorAnimatedNode.m index 3a86978d87d4ec..d35d6388651f3b 100644 --- a/Libraries/NativeAnimation/Nodes/RCTColorAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTColorAnimatedNode.m @@ -8,6 +8,8 @@ #import #import +#import + @implementation RCTColorAnimatedNode - (void)performUpdate @@ -19,8 +21,7 @@ - (void)performUpdate RCTValueAnimatedNode *bNode = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:self.config[@"b"]]; RCTValueAnimatedNode *aNode = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:self.config[@"a"]]; - _color = ((int)round(aNode.value * 255) & 0xff) << 24 | ((int)round(rNode.value) & 0xff) << 16 | - ((int)round(gNode.value) & 0xff) << 8 | ((int)round(bNode.value) & 0xff); + _color = RCTColorFromComponents(rNode.value, gNode.value, bNode.value, aNode.value); // TODO (T111179606): Support platform colors for color animations } diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h index 2ac165013dc06d..903b49286a2a5b 100644 --- a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h @@ -7,6 +7,16 @@ #import "RCTValueAnimatedNode.h" +#import + +RCT_EXTERN NSString *RCTInterpolateString( + NSString *pattern, + CGFloat inputValue, + NSArray *inputRange, + NSArray *> *outputRange, + NSString *extrapolateLeft, + NSString *extrapolateRight); + @interface RCTInterpolationAnimatedNode : RCTValueAnimatedNode @end diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m index 3950d4f15657fb..a11101781853bc 100644 --- a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m @@ -8,91 +8,116 @@ #import #import +#import static NSRegularExpression *regex; +typedef enum { + kNumber, + kColor, + kString, +} RCTInterpolationOutputType; + +static NSRegularExpression *getNumericComponentRegex() +{ + static NSRegularExpression *regex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *fpRegex = @"[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?"; + regex = [NSRegularExpression regularExpressionWithPattern:fpRegex + options:NSRegularExpressionCaseInsensitive + error:nil]; + }); + return regex; +} + +static NSArray *> *outputFromStringPattern(NSString *input) +{ + NSMutableArray *output = [NSMutableArray array]; + [getNumericComponentRegex() + enumerateMatchesInString:input + options:0 + range:NSMakeRange(0, input.length) + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + [output addObject:@([[input substringWithRange:result.range] doubleValue])]; + }]; + return output; +} + +NSString *RCTInterpolateString( + NSString *pattern, + CGFloat inputValue, + NSArray *inputRange, + NSArray *> *outputRange, + NSString *extrapolateLeft, + NSString *extrapolateRight) +{ + NSUInteger rangeIndex = RCTFindIndexOfNearestValue(inputValue, inputRange); + + NSMutableString *output = [NSMutableString stringWithString:pattern]; + NSArray *matches = + [getNumericComponentRegex() matchesInString:pattern options:0 range:NSMakeRange(0, pattern.length)]; + NSInteger matchIndex = matches.count - 1; + for (NSTextCheckingResult *match in [matches reverseObjectEnumerator]) { + CGFloat val = RCTInterpolateValue( + inputValue, + [inputRange[rangeIndex] doubleValue], + [inputRange[rangeIndex + 1] doubleValue], + [outputRange[rangeIndex][matchIndex] doubleValue], + [outputRange[rangeIndex + 1][matchIndex] doubleValue], + extrapolateLeft, + extrapolateRight); + [output replaceCharactersInRange:match.range withString:[@(val) stringValue]]; + matchIndex--; + } + return output; +} + @implementation RCTInterpolationAnimatedNode { __weak RCTValueAnimatedNode *_parentNode; NSArray *_inputRange; - NSArray *_outputRange; - NSArray *> *_outputs; - NSArray *_soutputRange; + NSArray *_outputRange; NSString *_extrapolateLeft; NSString *_extrapolateRight; - NSUInteger _numVals; - bool _hasStringOutput; - bool _shouldRound; + RCTInterpolationOutputType _outputType; + id _Nullable _outputvalue; + NSString *_Nullable _outputPattern; + NSArray *_matches; } - (instancetype)initWithTag:(NSNumber *)tag config:(NSDictionary *)config { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSString *fpRegex = @"[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?"; - regex = [NSRegularExpression regularExpressionWithPattern:fpRegex - options:NSRegularExpressionCaseInsensitive - error:nil]; - }); if ((self = [super initWithTag:tag config:config])) { - _inputRange = [config[@"inputRange"] copy]; - NSMutableArray *outputRange = [NSMutableArray array]; - NSMutableArray *soutputRange = [NSMutableArray array]; - NSMutableArray *> *_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]]; - } + _inputRange = config[@"inputRange"]; - _hasStringOutput = YES; - [outputRange addObject:[output objectAtIndex:0]]; - } + NSArray *outputRangeConfig = config[@"outputRange"]; + if ([config[@"outputType"] isEqual:@"color"]) { + _outputType = kColor; + } else if ([outputRangeConfig[0] isKindOfClass:[NSString class]]) { + _outputType = kString; + _outputPattern = outputRangeConfig[0]; + } else { + _outputType = kNumber; } - 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 *> *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]]; + + NSMutableArray *outputRange = [NSMutableArray arrayWithCapacity:outputRangeConfig.count]; + for (id value in outputRangeConfig) { + switch (_outputType) { + case kColor: { + UIColor *color = [RCTConvert UIColor:value]; + [outputRange addObject:color ? color : [UIColor whiteColor]]; + break; } + case kString: + [outputRange addObject:outputFromStringPattern(value)]; + break; + case kNumber: + [outputRange addObject:value]; + break; } - _outputs = [outputs copy]; } - _outputRange = [outputRange copy]; - _soutputRange = [soutputRange copy]; + _outputRange = outputRange; _extrapolateLeft = config[@"extrapolateLeft"]; _extrapolateRight = config[@"extrapolateRight"]; } @@ -123,43 +148,24 @@ - (void)performUpdate } CGFloat inputValue = _parentNode.value; - - 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 { - NSNumber *numberValue = [NSNumber numberWithDouble:val]; - str = [numberValue stringValue]; - } - - [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]]; - } + switch (_outputType) { + case kColor: + _outputvalue = @(RCTInterpolateColorInRange(inputValue, _inputRange, _outputRange)); + break; + case kString: + _outputvalue = RCTInterpolateString( + _outputPattern, inputValue, _inputRange, _outputRange, _extrapolateLeft, _extrapolateRight); + break; + case kNumber: + self.value = + RCTInterpolateValueInRange(inputValue, _inputRange, _outputRange, _extrapolateLeft, _extrapolateRight); + break; } } +- (id)animatedObject +{ + return _outputvalue; +} + @end diff --git a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m index e99992ee78dd90..180b2b1e7848a4 100644 --- a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m @@ -38,7 +38,12 @@ - (void)performUpdate if (node) { if ([node isKindOfClass:[RCTValueAnimatedNode class]]) { RCTValueAnimatedNode *valueAnimatedNode = (RCTValueAnimatedNode *)node; - _propsDictionary[property] = @(valueAnimatedNode.value); + id animatedObject = valueAnimatedNode.animatedObject; + if (animatedObject) { + _propsDictionary[property] = animatedObject; + } else { + _propsDictionary[property] = @(valueAnimatedNode.value); + } } else if ([node isKindOfClass:[RCTTransformAnimatedNode class]]) { RCTTransformAnimatedNode *transformAnimatedNode = (RCTTransformAnimatedNode *)node; [_propsDictionary addEntriesFromDictionary:transformAnimatedNode.propsDictionary]; diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h index ccc33462c0a840..84d220bb7b0dda 100644 --- a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h @@ -24,7 +24,7 @@ - (void)extractOffset; @property (nonatomic, assign) CGFloat value; -@property (nonatomic, strong) id animatedObject; +@property (nonatomic, strong, readonly) id animatedObject; @property (nonatomic, weak) id valueObserver; @end diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m index d128c9d92dd251..60d118cb085478 100644 --- a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m @@ -43,6 +43,11 @@ - (CGFloat)value return _value + _offset; } +- (id)animatedObject +{ + return nil; +} + - (void)setValue:(CGFloat)value { _value = value; diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.h b/Libraries/NativeAnimation/RCTAnimationUtils.h index 97594f35d87f00..338a88b48324c0 100644 --- a/Libraries/NativeAnimation/RCTAnimationUtils.h +++ b/Libraries/NativeAnimation/RCTAnimationUtils.h @@ -10,16 +10,11 @@ #import -static NSString *const EXTRAPOLATE_TYPE_IDENTITY = @"identity"; -static NSString *const EXTRAPOLATE_TYPE_CLAMP = @"clamp"; -static NSString *const EXTRAPOLATE_TYPE_EXTEND = @"extend"; +RCT_EXTERN NSString *const EXTRAPOLATE_TYPE_IDENTITY; +RCT_EXTERN NSString *const EXTRAPOLATE_TYPE_CLAMP; +RCT_EXTERN NSString *const EXTRAPOLATE_TYPE_EXTEND; -RCT_EXTERN CGFloat RCTInterpolateValueInRange( - CGFloat value, - NSArray *inputRange, - NSArray *outputRange, - NSString *extrapolateLeft, - NSString *extrapolateRight); +RCT_EXTERN NSUInteger RCTFindIndexOfNearestValue(CGFloat value, NSArray *range); RCT_EXTERN CGFloat RCTInterpolateValue( CGFloat value, @@ -30,8 +25,18 @@ RCT_EXTERN CGFloat RCTInterpolateValue( NSString *extrapolateLeft, NSString *extrapolateRight); -RCT_EXTERN CGFloat RCTRadiansToDegrees(CGFloat radians); -RCT_EXTERN CGFloat RCTDegreesToRadians(CGFloat degrees); +RCT_EXTERN CGFloat RCTInterpolateValueInRange( + CGFloat value, + NSArray *inputRange, + NSArray *outputRange, + NSString *extrapolateLeft, + NSString *extrapolateRight); + +RCT_EXTERN int32_t +RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange); + +// Represents a color as a int32_t. RGB components are assumed to be in [0-255] range and alpha in [0-1] range +RCT_EXTERN int32_t RCTColorFromComponents(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha); /** * Coefficient to slow down animations, respects the ios diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.m b/Libraries/NativeAnimation/RCTAnimationUtils.m index 1f97b22baf0846..22ee2bc90805df 100644 --- a/Libraries/NativeAnimation/RCTAnimationUtils.m +++ b/Libraries/NativeAnimation/RCTAnimationUtils.m @@ -9,7 +9,11 @@ #import -static NSUInteger _RCTFindIndexOfNearestValue(CGFloat value, NSArray *range) +NSString *const EXTRAPOLATE_TYPE_IDENTITY = @"identity"; +NSString *const EXTRAPOLATE_TYPE_CLAMP = @"clamp"; +NSString *const EXTRAPOLATE_TYPE_EXTEND = @"extend"; + +NSUInteger RCTFindIndexOfNearestValue(CGFloat value, NSArray *range) { NSUInteger index; NSUInteger rangeCount = range.count; @@ -71,7 +75,7 @@ CGFloat RCTInterpolateValueInRange( 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; @@ -80,14 +84,28 @@ CGFloat RCTInterpolateValueInRange( return RCTInterpolateValue(value, inputMin, inputMax, outputMin, outputMax, extrapolateLeft, extrapolateRight); } -CGFloat RCTRadiansToDegrees(CGFloat radians) +int32_t RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange) { - return radians * 180.0 / M_PI; + NSUInteger rangeIndex = RCTFindIndexOfNearestValue(value, inputRange); + CGFloat inputMin = inputRange[rangeIndex].doubleValue; + CGFloat inputMax = inputRange[rangeIndex + 1].doubleValue; + + CGFloat redMin, greenMin, blueMin, alphaMin; + [outputRange[rangeIndex] getRed:&redMin green:&greenMin blue:&blueMin alpha:&alphaMin]; + CGFloat redMax, greenMax, blueMax, alphaMax; + [outputRange[rangeIndex + 1] getRed:&redMax green:&greenMax blue:&blueMax alpha:&alphaMax]; + + return RCTColorFromComponents( + 0xFF * (redMin + (value - inputMin) * (redMax - redMin) / (inputMax - inputMin)), + 0xFF * (greenMin + (value - inputMin) * (greenMax - greenMin) / (inputMax - inputMin)), + 0xFF * (blueMin + (value - inputMin) * (blueMax - blueMin) / (inputMax - inputMin)), + alphaMin + (value - inputMin) * (alphaMax - alphaMin) / (inputMax - inputMin)); } -CGFloat RCTDegreesToRadians(CGFloat degrees) +int32_t RCTColorFromComponents(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) { - return degrees / 180.0 * M_PI; + return ((int)round(alpha * 255) & 0xFF) << 24 | ((int)round(red) & 0xFF) << 16 | ((int)round(green) & 0xFF) << 8 | + ((int)round(blue) & 0xFF); } #if TARGET_IPHONE_SIMULATOR diff --git a/packages/rn-tester/RNTesterUnitTests/RCTAnimationUtilsTests.m b/packages/rn-tester/RNTesterUnitTests/RCTAnimationUtilsTests.m index a9ae7df513da2a..5926ffd38278f1 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTAnimationUtilsTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTAnimationUtilsTests.m @@ -8,6 +8,8 @@ #import #import +#import +#import @interface RCTAnimationUtilsTests : XCTestCase @@ -93,4 +95,34 @@ - (void)testIdentityExtrapolate XCTAssertEqual(value, 5); } +- (void)testColorInterpolation +{ + NSArray *input = @[ @0, @1 ]; + NSArray *output = @[ [UIColor redColor], [UIColor blueColor] ]; + uint32_t value; + value = RCTInterpolateColorInRange(0, input, output); + XCTAssertEqualObjects([RCTConvert UIColor:@(value)], [UIColor redColor]); + value = RCTInterpolateColorInRange(0.5, input, output); + XCTAssertEqualObjects( + [RCTConvert UIColor:@(value)], [UIColor colorWithRed:128. / 255 green:0 blue:128. / 255 alpha:1]); + value = RCTInterpolateColorInRange(1, input, output); + XCTAssertEqualObjects([RCTConvert UIColor:@(value)], [UIColor blueColor]); +} + +- (void)testStringInterpolation +{ + NSString *pattern = @"M20,20L20,80L80,80L80,20Z"; + NSArray *input = @[ @0, @1 ]; + NSArray *> *output = @[ + @[ @20, @20, @20, @80, @80, @80, @80, @20 ], + @[ @40, @40, @33, @60, @60, @60, @65, @40 ], + ]; + + NSString *value; + value = RCTInterpolateString(pattern, 0, input, output, EXTRAPOLATE_TYPE_IDENTITY, EXTRAPOLATE_TYPE_IDENTITY); + XCTAssertEqualObjects(value, @"M20,20L20,80L80,80L80,20Z"); + value = RCTInterpolateString(pattern, 0.5, input, output, EXTRAPOLATE_TYPE_IDENTITY, EXTRAPOLATE_TYPE_IDENTITY); + XCTAssertEqualObjects(value, @"M30,30L26.5,70L70,70L72.5,30Z"); +} + @end