Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports[`processTransform validation should throw when passing an invalid angle

exports[`processTransform validation should throw when passing an invalid angle prop 4`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`;

exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be a number: {\\"translateY\\":\\"20deg\\"}"`;
exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be number or a percentage. Passed value: {\\"translateY\\":\\"20deg\\"}."`;

exports[`processTransform validation should throw when passing an invalid value to a number prop 2`] = `"Transform with key of \\"scale\\" must be a number: {\\"scale\\":{\\"x\\":10,\\"y\\":10}}"`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ describe('processTransform', () => {
);
});

it('should accept a percentage translate transform', () => {
processTransform([{translateY: '20%'}, {translateX: '10%'}]);
processTransform('translateX(10%)');
});

it('should throw on object with multiple properties', () => {
expect(() =>
processTransform([{scale: 0.5, translateY: 10}]),
Expand Down
21 changes: 18 additions & 3 deletions packages/react-native/Libraries/StyleSheet/processTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ const _getKeyAndValueFromCSSTransform: (
| $TEMPORARY$string<'translateX'>
| $TEMPORARY$string<'translateY'>,
args: string,
) => {key: string, value?: number[] | number | string} = (key, args) => {
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g);
) => {key: string, value?: Array<string | number> | number | string} = (
key,
args,
) => {
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+|%)?/g);

switch (key) {
case 'matrix':
Expand All @@ -88,7 +91,11 @@ const _getKeyAndValueFromCSSTransform: (
missingUnitOfMeasurement = true;
}

parsedArgs.push(value);
if (unitOfMeasurement === '%') {
parsedArgs.push(`${value}%`);
} else {
parsedArgs.push(value);
}
}

if (__DEV__) {
Expand Down Expand Up @@ -256,6 +263,14 @@ function _validateTransform(
break;
case 'translateX':
case 'translateY':
invariant(
typeof value === 'number' ||
(typeof value === 'string' && value.endsWith('%')),
'Transform with key of "%s" must be number or a percentage. Passed value: %s.',
key,
stringifySafe(transformation),
);
break;
case 'scale':
case 'scaleX':
case 'scaleY':
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/React/Views/RCTConvert+Transform.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

+ (CATransform3D)CATransform3D:(id)json;

+ (CATransform3D)CATransform3D:(id)json withWidth:(CGFloat)width height:(CGFloat)height;

+ (RCTTransformOrigin)RCTTransformOrigin:(id)json;

@end
205 changes: 116 additions & 89 deletions packages/react-native/React/Views/RCTConvert+Transform.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,102 +48,129 @@ + (CATransform3D)CATransform3DFromMatrix:(id)json

+ (CATransform3D)CATransform3D:(id)json
{
CATransform3D transform = CATransform3DIdentity;
if (!json) {
return transform;
}
if (![json isKindOfClass:[NSArray class]]) {
RCTLogConvertError(json, @"a CATransform3D. Did you pass something other than an array?");
return transform;
}
// legacy matrix support
if ([(NSArray *)json count] == kMatrixArrayLength && [json[0] isKindOfClass:[NSNumber class]]) {
RCTLogWarn(
@"[RCTConvert CATransform3D:] has deprecated a matrix as input. Pass an array of configs (which can contain a matrix key) instead.");
return [self CATransform3DFromMatrix:json];
}

CGFloat zeroScaleThreshold = FLT_EPSILON;
return [self CATransform3D:json withWidth:NAN height:NAN];
}

CATransform3D next;
for (NSDictionary *transformConfig in (NSArray<NSDictionary *> *)json) {
if (transformConfig.count != 1) {
RCTLogConvertError(json, @"a CATransform3D. You must specify exactly one property per transform object.");
+ (CATransform3D)CATransform3D:(id)json withWidth:(CGFloat)width height:(CGFloat)height {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have [RCTConvert CATransform3D:] call [RCTConvert CATransform3D:json withWidth:NaN height:NaN] or something similar? We don't want to be duplicating this code.

Any caller to the other CATransform should error or fallback to zero when width/height are not provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks. Added an error log and returned 0 when height/width is not provided for percentage values.

{
CATransform3D transform = CATransform3DIdentity;
if (!json) {
return transform;
}
NSString *property = transformConfig.allKeys[0];
id value = transformConfig[property];

if ([property isEqualToString:@"matrix"]) {
next = [self CATransform3DFromMatrix:value];
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"perspective"]) {
next = CATransform3DIdentity;
next.m34 = -1 / [value floatValue];
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"rotateX"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 1, 0, 0);

} else if ([property isEqualToString:@"rotateY"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 0, 1, 0);

} else if ([property isEqualToString:@"rotate"] || [property isEqualToString:@"rotateZ"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 0, 0, 1);

} else if ([property isEqualToString:@"scale"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, scale, scale, 1);

} else if ([property isEqualToString:@"scaleX"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, scale, 1, 1);

} else if ([property isEqualToString:@"scaleY"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, 1, scale, 1);

} else if ([property isEqualToString:@"translate"]) {
NSArray *array = (NSArray<NSNumber *> *)value;
CGFloat translateX = [array[0] floatValue];
CGFloat translateY = [array[1] floatValue];
CGFloat translateZ = array.count > 2 ? [array[2] floatValue] : 0;
transform = CATransform3DTranslate(transform, translateX, translateY, translateZ);

} else if ([property isEqualToString:@"translateX"]) {
CGFloat translate = [value floatValue];
transform = CATransform3DTranslate(transform, translate, 0, 0);

} else if ([property isEqualToString:@"translateY"]) {
CGFloat translate = [value floatValue];
transform = CATransform3DTranslate(transform, 0, translate, 0);

} else if ([property isEqualToString:@"skewX"]) {
CGFloat skew = [self convertToRadians:value];
next = CATransform3DIdentity;
next.m21 = tanf(skew);
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"skewY"]) {
CGFloat skew = [self convertToRadians:value];
next = CATransform3DIdentity;
next.m12 = tanf(skew);
transform = CATransform3DConcat(next, transform);

} else {
RCTLogInfo(@"Unsupported transform type for a CATransform3D: %@.", property);
if (![json isKindOfClass:[NSArray class]]) {
RCTLogConvertError(json, @"a CATransform3D. Did you pass something other than an array?");
return transform;
}
// legacy matrix support
if ([(NSArray *)json count] == kMatrixArrayLength && [json[0] isKindOfClass:[NSNumber class]]) {
RCTLogWarn(
@"[RCTConvert CATransform3D:] has deprecated a matrix as input. Pass an array of configs (which can contain a matrix key) instead.");
return [self CATransform3DFromMatrix:json];
}

CGFloat zeroScaleThreshold = FLT_EPSILON;

CATransform3D next;
for (NSDictionary *transformConfig in (NSArray<NSDictionary *> *)json) {
if (transformConfig.count != 1) {
RCTLogConvertError(json, @"a CATransform3D. You must specify exactly one property per transform object.");
return transform;
}
NSString *property = transformConfig.allKeys[0];
id value = transformConfig[property];

if ([property isEqualToString:@"matrix"]) {
next = [self CATransform3DFromMatrix:value];
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"perspective"]) {
next = CATransform3DIdentity;
next.m34 = -1 / [value floatValue];
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"rotateX"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 1, 0, 0);

} else if ([property isEqualToString:@"rotateY"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 0, 1, 0);

} else if ([property isEqualToString:@"rotate"] || [property isEqualToString:@"rotateZ"]) {
CGFloat rotate = [self convertToRadians:value];
transform = CATransform3DRotate(transform, rotate, 0, 0, 1);

} else if ([property isEqualToString:@"scale"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, scale, scale, 1);

} else if ([property isEqualToString:@"scaleX"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, scale, 1, 1);

} else if ([property isEqualToString:@"scaleY"]) {
CGFloat scale = [value floatValue];
scale = ABS(scale) < zeroScaleThreshold ? zeroScaleThreshold : scale;
transform = CATransform3DScale(transform, 1, scale, 1);

} else if ([property isEqualToString:@"translate"]) {
NSArray *array = (NSArray *)value;
CGFloat translateX = [self parseTranslate:array[0] withDimension:width];
CGFloat translateY = [self parseTranslate:array[1] withDimension:height];
CGFloat translateZ = array.count > 2 ? [array[2] floatValue] : 0;
transform = CATransform3DTranslate(transform, translateX, translateY, translateZ);

} else if ([property isEqualToString:@"translateX"]) {
CGFloat translate = [self parseTranslate:value withDimension:width];
transform = CATransform3DTranslate(transform, translate, 0, 0);

} else if ([property isEqualToString:@"translateY"]) {
CGFloat translate = [self parseTranslate:value withDimension:height];
transform = CATransform3DTranslate(transform, 0, translate, 0);

} else if ([property isEqualToString:@"skewX"]) {
CGFloat skew = [self convertToRadians:value];
next = CATransform3DIdentity;
next.m21 = tanf(skew);
transform = CATransform3DConcat(next, transform);

} else if ([property isEqualToString:@"skewY"]) {
CGFloat skew = [self convertToRadians:value];
next = CATransform3DIdentity;
next.m12 = tanf(skew);
transform = CATransform3DConcat(next, transform);

} else {
RCTLogInfo(@"Unsupported transform type for a CATransform3D: %@.", property);
}
}
return transform;
}
return transform;
}

+ (CGFloat)parseTranslate:(id)value withDimension:(CGFloat)dimension {
if ([value isKindOfClass:[NSNumber class]]) {
return [value floatValue];
} else if ([value isKindOfClass:[NSString class]]) {
NSString *stringValue = (NSString *)value;
if ([stringValue hasSuffix:@"%"]) {
if (isnan(dimension)) {
RCTLogConvertError(value, @"a CATransform3D. Expected dimension to be available for percentage calculation.");
return 0;
}
NSString *percentageString = [stringValue substringToIndex:stringValue.length - 1];
CGFloat percentage = [percentageString floatValue] / 100.0f;
return dimension * percentage;
} else {
return [stringValue floatValue];
}
}
return 0;
}


+ (RCTTransformOrigin)RCTTransformOrigin:(id)json
{
RCTTransformOrigin transformOrigin = {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ - (RCTShadowView *)shadowView
view.layer.shouldRasterize ? view.traitCollection.displayScale : defaultView.layer.rasterizationScale;
}

RCT_REMAP_VIEW_PROPERTY(transform, reactTransform, CATransform3D)
RCT_REMAP_VIEW_PROPERTY(transform, reactTransform, NSArray)
RCT_REMAP_VIEW_PROPERTY(transformOrigin, reactTransformOrigin, RCTTransformOrigin)

RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView)
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/UIView+React.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ typedef struct {
* The anchorPoint property doesn't work in the same way as on web - updating it updates the frame.
* To work around this, we take both the transform and the transform-origin, and compute it ourselves
*/
@property (nonatomic, assign) CATransform3D reactTransform;
@property (nonatomic, retain) NSArray* reactTransform;
@property (nonatomic, assign) RCTTransformOrigin reactTransformOrigin;

/**
Expand Down
34 changes: 20 additions & 14 deletions packages/react-native/React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import "RCTAssert.h"
#import "RCTLog.h"
#import "RCTShadowView.h"
#import "RCTConvert+Transform.h"

@implementation UIView (React)

Expand Down Expand Up @@ -202,25 +203,28 @@ - (void)reactSetFrame:(CGRect)frame
}

self.center = position;
self.bounds = bounds;

id transformOrigin = objc_getAssociatedObject(self, @selector(reactTransformOrigin));
if (transformOrigin) {
updateTransform(self);

if (!CGRectEqualToRect(self.bounds, bounds)) {
self.bounds = bounds;
id transformOrigin = objc_getAssociatedObject(self, @selector(reactTransformOrigin));
id transform = objc_getAssociatedObject(self, @selector(reactTransform));
if (transform || transformOrigin) {
updateTransform(self);
}
}
}

#pragma mark - Transforms

- (CATransform3D)reactTransform
- (NSArray*)reactTransform
{
id obj = objc_getAssociatedObject(self, _cmd);
return obj != nil ? [obj CATransform3DValue] : CATransform3DIdentity;
return obj;
}

- (void)setReactTransform:(CATransform3D)reactTransform
- (void)setReactTransform:(NSArray*)reactTransform
{
objc_setAssociatedObject(self, @selector(reactTransform), @(reactTransform), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, @selector(reactTransform), reactTransform, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
updateTransform(self);
}

Expand All @@ -245,10 +249,14 @@ - (void)setReactTransformOrigin:(RCTTransformOrigin)reactTransformOrigin

static void updateTransform(UIView *view)
{
CATransform3D transform;
CGSize size = view.bounds.size;
if (CGSizeEqualToSize(CGSizeZero, size)) {
return;
};

CATransform3D transform = [RCTConvert CATransform3D: view.reactTransform withWidth: size.width height:size.height];
id rawTansformOrigin = objc_getAssociatedObject(view, @selector(reactTransformOrigin));
if (rawTansformOrigin) {
CGSize size = view.bounds.size;
CGFloat anchorPointX = 0;
CGFloat anchorPointY = 0;
CGFloat anchorPointZ = 0;
Expand All @@ -267,11 +275,9 @@ static void updateTransform(UIView *view)
}
anchorPointZ = transformOrigin.z;
transform = CATransform3DConcat(
view.reactTransform, CATransform3DMakeTranslation(anchorPointX, anchorPointY, anchorPointZ));
transform, CATransform3DMakeTranslation(anchorPointX, anchorPointY, anchorPointZ));
transform =
CATransform3DConcat(CATransform3DMakeTranslation(-anchorPointX, -anchorPointY, -anchorPointZ), transform);
} else {
transform = view.reactTransform;
}

view.layer.transform = transform;
Expand Down
Loading