Skip to content

Commit

Permalink
Support colors for AnimatedInterpolation on iOS
Browse files Browse the repository at this point in the history
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
  • Loading branch information
javache authored and facebook-github-bot committed Nov 30, 2022
1 parent e7dbfb2 commit 6003e70
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 123 deletions.
5 changes: 3 additions & 2 deletions Libraries/NativeAnimation/Nodes/RCTColorAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#import <React/RCTColorAnimatedNode.h>
#import <React/RCTValueAnimatedNode.h>

#import <React/RCTAnimationUtils.h>

@implementation RCTColorAnimatedNode

- (void)performUpdate
Expand All @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

#import "RCTValueAnimatedNode.h"

#import <React/RCTDefines.h>

RCT_EXTERN NSString *RCTInterpolateString(
NSString *pattern,
CGFloat inputValue,
NSArray *inputRange,
NSArray<NSArray<NSNumber *> *> *outputRange,
NSString *extrapolateLeft,
NSString *extrapolateRight);

@interface RCTInterpolationAnimatedNode : RCTValueAnimatedNode

@end
210 changes: 108 additions & 102 deletions Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,91 +8,116 @@
#import <React/RCTInterpolationAnimatedNode.h>

#import <React/RCTAnimationUtils.h>
#import <React/RCTConvert.h>

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<NSArray<NSNumber *> *> *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<NSNumber *> *inputRange,
NSArray<NSArray<NSNumber *> *> *outputRange,
NSString *extrapolateLeft,
NSString *extrapolateRight)
{
NSUInteger rangeIndex = RCTFindIndexOfNearestValue(inputValue, inputRange);

NSMutableString *output = [NSMutableString stringWithString:pattern];
NSArray<NSTextCheckingResult *> *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<NSNumber *> *_inputRange;
NSArray<NSNumber *> *_outputRange;
NSArray<NSArray<NSNumber *> *> *_outputs;
NSArray<NSString *> *_soutputRange;
NSArray<id> *_outputRange;
NSString *_extrapolateLeft;
NSString *_extrapolateRight;
NSUInteger _numVals;
bool _hasStringOutput;
bool _shouldRound;
RCTInterpolationOutputType _outputType;
id _Nullable _outputvalue;
NSString *_Nullable _outputPattern;

NSArray<NSTextCheckingResult *> *_matches;
}

- (instancetype)initWithTag:(NSNumber *)tag config:(NSDictionary<NSString *, id> *)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<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]];
}
_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<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]];

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"];
}
Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<RCTValueAnimatedNodeObserver> valueObserver;

@end
5 changes: 5 additions & 0 deletions Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ - (CGFloat)value
return _value + _offset;
}

- (id)animatedObject
{
return nil;
}

- (void)setValue:(CGFloat)value
{
_value = value;
Expand Down
27 changes: 16 additions & 11 deletions Libraries/NativeAnimation/RCTAnimationUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@

#import <React/RCTDefines.h>

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<NSNumber *> *inputRange,
NSArray<NSNumber *> *outputRange,
NSString *extrapolateLeft,
NSString *extrapolateRight);
RCT_EXTERN NSUInteger RCTFindIndexOfNearestValue(CGFloat value, NSArray<NSNumber *> *range);

RCT_EXTERN CGFloat RCTInterpolateValue(
CGFloat value,
Expand All @@ -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<NSNumber *> *inputRange,
NSArray<NSNumber *> *outputRange,
NSString *extrapolateLeft,
NSString *extrapolateRight);

RCT_EXTERN int32_t
RCTInterpolateColorInRange(CGFloat value, NSArray<NSNumber *> *inputRange, NSArray<UIColor *> *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
Expand Down
Loading

0 comments on commit 6003e70

Please sign in to comment.