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 @@ -111,6 +111,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
* Transform
*/
transform: {process: processTransform},
transformOrigin: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Use this to pre-process it on the JS side so you can share it with the Android side - https://gist.github.com/jacobp100/86bc3fa863e41f42ca091386f6252f29 - I maintain css-to-react-native so you can trust me 🤣

It gives you an array with 3 components corresponding to x, y, and z. Gives either a number ([json isKindOfClass:NSNumber.class]), or a string with a % at the end (re-use your existing logic here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, had that in mind 😅, was saving for the last. I'll check your code! Thanks!


/**
* View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const validAttributesForNonEventProps = {
overflow: true,
shouldRasterizeIOS: true,
transform: {diff: require('../Utilities/differ/matricesDiffer')},
transformOrigin: true,
accessibilityRole: true,
accessibilityState: true,
nativeID: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function splitLayoutProps(props: ?____ViewStyle_Internal): {
case 'bottom':
case 'top':
case 'transform':
case 'transformOrigin':
case 'rowGap':
case 'columnGap':
case 'gap':
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTConvert+Transform.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
@interface RCTConvert (Transform)

+ (CATransform3D)CATransform3D:(id)json;
+ (CATransform3D)CATransform3D:(id)json viewWidth: (CGFloat) viewWidth viewHeight: (CGFloat) viewHeight transformOrigin: (NSString*) transformOrigin;

@end
57 changes: 57 additions & 0 deletions packages/react-native/React/Views/RCTConvert+Transform.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,46 @@

static const NSUInteger kMatrixArrayLength = 4 * 4;

static NSArray* getTranslateForTransformOrigin(CGFloat viewWidth, CGFloat viewHeight, NSString *transformOrigin) {
if (transformOrigin.length == 0 || (viewWidth == 0 && viewHeight == 0)) {
return nil;
}

CGFloat viewCenterX = viewWidth / 2;
CGFloat viewCenterY = viewHeight / 2;

CGFloat origin[3] = {viewCenterX, viewCenterY, 0.0};

NSArray *parts = [transformOrigin componentsSeparatedByString:@" "];
for (NSInteger i = 0; i < parts.count && i < 3; i++) {
NSString *part = parts[i];
NSRange percentRange = [part rangeOfString:@"%"];
BOOL isPercent = percentRange.location != NSNotFound;
if (isPercent) {
CGFloat val = [[part substringToIndex:percentRange.location] floatValue];
origin[i] = (i == 0 ? viewWidth : viewHeight) * val / 100.0;
} else if ([part isEqualToString:@"top"]) {
origin[1] = 0.0;
} else if ([part isEqualToString:@"bottom"]) {
origin[1] = viewHeight;
} else if ([part isEqualToString:@"left"]) {
origin[0] = 0.0;
} else if ([part isEqualToString:@"right"]) {
origin[0] = viewWidth;
} else if ([part isEqualToString:@"center"]) {
continue;
} else {
origin[i] = [part floatValue];
}
}

CGFloat newTranslateX = -viewCenterX + origin[0];
CGFloat newTranslateY = -viewCenterY + origin[1];
CGFloat newTranslateZ = origin[2];

return @[@(newTranslateX), @(newTranslateY), @(newTranslateZ)];
}

@implementation RCTConvert (Transform)

+ (CGFloat)convertToRadians:(id)json
Expand Down Expand Up @@ -47,6 +87,12 @@ + (CATransform3D)CATransform3DFromMatrix:(id)json
}

+ (CATransform3D)CATransform3D:(id)json
{
CATransform3D transform = [self CATransform3D:json viewWidth:0 viewHeight:0 transformOrigin:nil];
return transform;
}

+ (CATransform3D)CATransform3D:(id)json viewWidth: (CGFloat) viewWidth viewHeight: (CGFloat) viewHeight transformOrigin: (NSString*) transformOrigin
{
CATransform3D transform = CATransform3DIdentity;
if (!json) {
Expand All @@ -66,6 +112,12 @@ + (CATransform3D)CATransform3D:(id)json
CGFloat zeroScaleThreshold = FLT_EPSILON;

CATransform3D next;

NSArray *offsets = getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin);
if (offsets) {
transform = CATransform3DTranslate(transform, [offsets[0] floatValue], [offsets[1] floatValue], [offsets[2] floatValue]);
}

for (NSDictionary *transformConfig in (NSArray<NSDictionary *> *)json) {
if (transformConfig.count != 1) {
RCTLogConvertError(json, @"a CATransform3D. You must specify exactly one property per transform object.");
Expand Down Expand Up @@ -141,6 +193,11 @@ + (CATransform3D)CATransform3D:(id)json
RCTLogInfo(@"Unsupported transform type for a CATransform3D: %@.", property);
}
}

if (offsets) {
transform = CATransform3DTranslate(transform, -[offsets[0] floatValue], -[offsets[1] floatValue], -[offsets[2] floatValue]);
}

return transform;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
@property (nonatomic, assign) RCTBubblingEventBlock onGotPointerCapture;
@property (nonatomic, assign) RCTBubblingEventBlock onLostPointerCapture;

@property (nonatomic, strong) id rawTransform;
@property (nonatomic, copy) NSString* transformOrigin;

@end
5 changes: 5 additions & 0 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import "RCTLog.h"
#import "RCTViewUtils.h"
#import "UIView+React.h"
#import "RCTConvert+Transform.h"

RCT_MOCK_DEF(RCTView, RCTContentInsets);
#define RCTContentInsets RCT_MOCK_USE(RCTView, RCTContentInsets)
Expand Down Expand Up @@ -785,6 +786,10 @@ - (void)reactSetFrame:(CGRect)frame
[super reactSetFrame:frame];
if (!CGSizeEqualToSize(self.bounds.size, oldSize)) {
[self.layer setNeedsDisplay];
// Update transform for transform origin due to change in view dimension
if (self.transformOrigin.length > 0) {
self.layer.transform = [RCTConvert CATransform3D:self.rawTransform viewWidth:self.bounds.size.width viewHeight:self.bounds.size.height transformOrigin: self.transformOrigin];
Copy link
Contributor

@jacobp100 jacobp100 Jul 21, 2023

Choose a reason for hiding this comment

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

I think you can use anchorPoint instead. It'd save you having to store rawTransform as a property

Note that it's a percent value rather than pixel, so if you do this you'll need to flip your logic so your non-percent values are multiplied by the view width/height. Also note that it only supports x and y, and there's anchorPointZ for the z-axis

Copy link
Contributor Author

@intergalacticspacehighway intergalacticspacehighway Jul 21, 2023

Choose a reason for hiding this comment

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

The initial version that I tried used anchorPoint but anchorPoint also changes the position of the view and it might conflict with yoga's setting position or will need some handling so I dropped that idea, also Android has pivot x and pivot y but no pivot z so thought of using the raw translates to achieve transform-origin and keeping it consistent on android and iOS. 😅

}
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,18 @@ - (RCTShadowView *)shadowView

RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
{
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
view.rawTransform = json;
view.layer.transform = json ? [RCTConvert CATransform3D:view.rawTransform viewWidth:view.bounds.size.width viewHeight:view.bounds.size.height transformOrigin: view.transformOrigin] : defaultView.layer.transform;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this so you can use %s in the transform property?

Copy link
Contributor Author

@intergalacticspacehighway intergalacticspacehighway Jul 21, 2023

Choose a reason for hiding this comment

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

yeah exactly, also center, right etc enums require height and width of the view.

// Enable edge antialiasing in rotation, skew, or perspective transforms
view.layer.allowsEdgeAntialiasing =
view.layer.transform.m12 != 0.0f || view.layer.transform.m21 != 0.0f || view.layer.transform.m34 != 0.0f;
}

RCT_CUSTOM_VIEW_PROPERTY(transformOrigin, NSString, RCTView){
view.transformOrigin = json;
view.layer.transform = [RCTConvert CATransform3D:view.rawTransform viewWidth:view.bounds.size.width viewHeight:view.bounds.size.height transformOrigin: view.transformOrigin];
}

RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView)
{
UIAccessibilityTraits accessibilityRoleTraits =
Expand Down
51 changes: 50 additions & 1 deletion packages/rn-tester/js/examples/Transform/TransformExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import React, {useEffect, useState} from 'react';
import {Animated, StyleSheet, Text, View} from 'react-native';
import {Animated, StyleSheet, Text, View, Easing} from 'react-native';

import type {Node, Element} from 'react';

Expand Down Expand Up @@ -50,6 +50,39 @@ function AnimateTransformSingleProp() {
);
}

function TransformOriginExample() {
const rotateAnim = React.useRef(new Animated.Value(0)).current;

useEffect(() => {
Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
}),
).start();
}, [rotateAnim]);

const spin = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});

return (
<View style={styles.transformOriginWrapper}>
<Animated.View
style={[
styles.transformOriginView,
{
transform: [{rotate: spin}],
},
]}
/>
</View>
);
}

function Flip() {
const [theta] = useState(new Animated.Value(45));
const animate = () => {
Expand Down Expand Up @@ -234,6 +267,15 @@ const styles = StyleSheet.create({
color: 'white',
fontWeight: 'bold',
},
transformOriginWrapper: {
alignItems: 'center',
},
transformOriginView: {
backgroundColor: 'pink',
width: 100,
height: 100,
transformOrigin: 'top',
},
});

exports.title = 'Transforms';
Expand Down Expand Up @@ -346,4 +388,11 @@ exports.examples = [
);
},
},
{
title: 'Transform origin',
description: "transformOrigin: 'top'",
render(): Node {
return <TransformOriginExample />;
},
},
];