Skip to content

Commit

Permalink
Implement transform-origin for old arch iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobp100 committed Jul 25, 2023
1 parent e64756a commit f81b4f1
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import processAspectRatio from '../../StyleSheet/processAspectRatio';
import processColor from '../../StyleSheet/processColor';
import processFontVariant from '../../StyleSheet/processFontVariant';
import processTransform from '../../StyleSheet/processTransform';
import processTransformOrigin from '../../StyleSheet/processTransformOrigin';
import sizesDiffer from '../../Utilities/differ/sizesDiffer';

const colorAttributes = {process: processColor};
Expand Down Expand Up @@ -111,6 +112,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
* Transform
*/
transform: {process: processTransform},
transformOrigin: {process: processTransformOrigin},

/**
* 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 @@ -196,6 +196,7 @@ export interface TransformsStyle {
>[]
| string
| undefined;
transformOrigin?: Array<string | number> | string | undefined;
/**
* @deprecated Use matrix in transform prop instead.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`processTransformOrigin validation only accepts three values 1`] = `"Transform origin must have exactly 3 values."`;

exports[`processTransformOrigin validation only accepts three values 2`] = `"Transform origin must have exactly 3 values."`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

import processTransformOrigin from '../processTransformOrigin';

describe('processTransformOrigin', () => {
describe('validation', () => {
it('only accepts three values', () => {
expect(() => {
processTransformOrigin([]);
}).toThrowErrorMatchingSnapshot();
expect(() => {
processTransformOrigin(['50%', '50%']);
}).toThrowErrorMatchingSnapshot();
});

it('should transform a string', () => {
expect(processTransformOrigin('50% 50% 5px')).toEqual(['50%', '50%', 5]);
});

it('should handle one value', () => {
expect(processTransformOrigin('top')).toEqual(['50%', 0, 0]);
expect(processTransformOrigin('right')).toEqual(['100%', '50%', 0]);
expect(processTransformOrigin('bottom')).toEqual(['50%', '100%', 0]);
expect(processTransformOrigin('left')).toEqual([0, '50%', 0]);
});

it('should handle two values', () => {
expect(processTransformOrigin('30% top')).toEqual(['30%', 0, 0]);
expect(processTransformOrigin('right 30%')).toEqual(['100%', '30%', 0]);
expect(processTransformOrigin('30% bottom')).toEqual(['30%', '100%', 0]);
expect(processTransformOrigin('left 30%')).toEqual([0, '30%', 0]);
});

it('should handle two keywords in either order', () => {
expect(processTransformOrigin('right bottom')).toEqual([
'100%',
'100%',
0,
]);
expect(processTransformOrigin('bottom right')).toEqual([
'100%',
'100%',
0,
]);
expect(processTransformOrigin('right bottom 5px')).toEqual([
'100%',
'100%',
5,
]);
expect(processTransformOrigin('bottom right 5px')).toEqual([
'100%',
'100%',
5,
]);
});

it('should not allow specifying same position twice', () => {
expect(() => {
processTransformOrigin('top top');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top top"`,
);
expect(() => {
processTransformOrigin('right right');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin right can only be used for x-position"`,
);
expect(() => {
processTransformOrigin('bottom bottom');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: bottom bottom"`,
);
expect(() => {
processTransformOrigin('left left');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin left can only be used for x-position"`,
);
expect(() => {
processTransformOrigin('top bottom');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top bottom"`,
);
expect(() => {
processTransformOrigin('left right');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin right can only be used for x-position"`,
);
});

it('should handle three values', () => {
expect(processTransformOrigin('30% top 10px')).toEqual(['30%', 0, 10]);
expect(processTransformOrigin('right 30% 10px')).toEqual([
'100%',
'30%',
10,
]);
expect(processTransformOrigin('30% bottom 10px')).toEqual([
'30%',
'100%',
10,
]);
expect(processTransformOrigin('left 30% 10px')).toEqual([0, '30%', 10]);
});

it('should enforce two value ordering', () => {
expect(() => {
processTransformOrigin('top 30%');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30%"`,
);
});

it('should not allow percents for z-position', () => {
expect(() => {
processTransformOrigin('top 30% 30%');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30% 30%"`,
);
expect(() => {
processTransformOrigin('top 30% center');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30% center"`,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,16 @@ export type ____TransformStyle_Internal = $ReadOnly<{|
|},
>
| string,
/**
* `transformOrigin` accepts an array with 3 elements - each element either being
* a number, or a string of a number ending with `%`. The last element cannot be
* a percentage, so must be a number.
*
* E.g. transformOrigin: ['30%', '80%', 15]
*
* Alternatively accepts a string of the CSS syntax. You must use `%` or `px`.
*
* E.g. transformOrigin: '30% 80% 15px'
*/
transformOrigin?: Array<string | number> | string,
|}>;
135 changes: 135 additions & 0 deletions packages/react-native/Libraries/StyleSheet/processTransformOrigin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

import invariant from 'invariant';

const INDEX_X = 0;
const INDEX_Y = 1;
const INDEX_Z = 2;

/* eslint-disable no-labels */
export default function processTransformOrigin(
transformOrigin: Array<string | number> | string,
): Array<string | number> {
if (typeof transformOrigin === 'string') {
const regExp = /(top|bottom|left|right|center|\d+(?:%|px)|0)/gi;
const transformOriginArray: Array<string | number> = ['50%', '50%', 0];

let index = INDEX_X;
let match;
outer: while ((match = regExp.exec(transformOrigin))) {
let nextIndex = index + 1;

const value = match[0];
const valueLower = value.toLowerCase();

switch (valueLower) {
case 'left':
case 'right': {
invariant(
index === INDEX_X,
'Transform-origin %s can only be used for x-position',
value,
);
transformOriginArray[INDEX_X] = valueLower === 'left' ? 0 : '100%';
break;
}
case 'top':
case 'bottom': {
invariant(
index !== INDEX_Z,
'Transform-origin %s can only be used for y-position',
value,
);
transformOriginArray[INDEX_Y] = valueLower === 'top' ? 0 : '100%';

// Handle [[ center | left | right ] && [ center | top | bottom ]] <length>?
if (index === INDEX_X) {
const horizontal = regExp.exec(transformOrigin);
if (horizontal == null) {
break outer;
}

switch (horizontal[0].toLowerCase()) {
case 'left':
transformOriginArray[INDEX_X] = 0;
break;
case 'right':
transformOriginArray[INDEX_X] = '100%';
break;
case 'center':
transformOriginArray[INDEX_X] = '50%';
break;
default:
invariant(
false,
'Could not parse transform-origin: %s',
transformOrigin,
);
}
nextIndex = INDEX_Z;
}

break;
}
case 'center': {
invariant(
index !== INDEX_Z,
'Transform-origin value %s cannot be used for z-position',
value,
);
transformOriginArray[index] = '50%';
break;
}
default: {
if (value.endsWith('%')) {
transformOriginArray[index] = value;
} else {
transformOriginArray[index] = parseFloat(value); // Remove `px`
}
break;
}
}

index = nextIndex;
}

transformOrigin = transformOriginArray;
}

if (__DEV__) {
_validateTransformOrigin(transformOrigin);
}

return transformOrigin;
}

function _validateTransformOrigin(transformOrigin: Array<string | number>) {
invariant(
transformOrigin.length === 3,
'Transform origin must have exactly 3 values.',
);
const [x, y, z] = transformOrigin;
invariant(
typeof x === 'number' || (typeof x === 'string' && x.endsWith('%')),
'Transform origin x-position must be a number. Passed value: %s.',
x,
);
invariant(
typeof y === 'number' || (typeof y === 'string' && y.endsWith('%')),
'Transform origin y-position must be a number. Passed value: %s.',
y,
);
invariant(
typeof z === 'number',
'Transform origin z-position must be a number. Passed value: %s.',
z,
);
}
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
36 changes: 32 additions & 4 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ @implementation RCTConvert (UIAccessibilityTraits)
UIAccessibilityTraitNone,
unsignedLongLongValue)

+ (CATransform3D)transformOrigin:(id)json
{
CATransform3D transformOrigin = CATransform3DMakeScale(0, 0, 0);
id anchorPointX = json[0];
id anchorPointY = json[1];
id anchorPointZ = json[2];

if ([anchorPointX isKindOfClass:NSString.class] && [(NSString *)anchorPointX hasSuffix:@"%"]) {
transformOrigin.m11 = [anchorPointX doubleValue] / 100;
} else {
transformOrigin.m14 = [RCTConvert CGFloat:anchorPointX];
}

if ([anchorPointY isKindOfClass:NSString.class] && [(NSString *)anchorPointY hasSuffix:@"%"]) {
transformOrigin.m22 = [anchorPointY doubleValue] / 100;
} else {
transformOrigin.m24 = [RCTConvert CGFloat:anchorPointY];
}

transformOrigin.m34 = [RCTConvert CGFloat:anchorPointZ];

return transformOrigin;
}

@end

@implementation RCTViewManager
Expand Down Expand Up @@ -218,10 +242,14 @@ - (RCTShadowView *)shadowView

RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
{
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
// 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;
CATransform3D transform = json ? [RCTConvert CATransform3D:json] : defaultView.reactTransformOrigin;
[view setReactTransform:transform];
}

RCT_CUSTOM_VIEW_PROPERTY(transformOrigin, NSArray, RCTView)
{
CATransform3D transformOrigin = json ? [RCTConvert transformOrigin:json] : defaultView.reactTransformOrigin;
[view setReactTransformOrigin:transformOrigin];
}

RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView)
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native/React/Views/UIView+React.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@
@property (nonatomic, readonly) UIEdgeInsets reactCompoundInsets;
@property (nonatomic, readonly) CGRect reactContentFrame;

@property (nonatomic, assign) CATransform3D reactTransform;
/**
* Matrix form of transform-origin.
* Vector form is calculated by multiplying matrix with the vector `[width, height, 0]`.
*/
@property (nonatomic, assign) CATransform3D reactTransformOrigin;

/**
* The (sub)view which represents this view in terms of accessibility.
* ViewManager will apply all accessibility properties directly to this view.
Expand Down
Loading

0 comments on commit f81b4f1

Please sign in to comment.