Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: transform-origin #37606

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,
rozele marked this conversation as resolved.
Show resolved Hide resolved

/**
* View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const validAttributesForNonEventProps = {
// @ReactProps from BaseViewManager
backgroundColor: {process: require('../StyleSheet/processColor').default},
transform: true,
transformOrigin: true,
opacity: true,
elevation: true,
shadowColor: {process: require('../StyleSheet/processColor').default},
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
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
}

// `transform`
if (oldViewProps.transform != newViewProps.transform &&
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
self.layer.transform = RCTCATransform3DFromTransformMatrix(newViewProps.transform);
if ((oldViewProps.transform != newViewProps.transform || oldViewProps.transformOrigin != newViewProps.transformOrigin)
&& ![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
auto newTransform = newViewProps.resolveTransform(_layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
self.layer.allowsEdgeAntialiasing = newViewProps.transform != Transform::Identity();
}

Expand Down Expand Up @@ -397,6 +398,12 @@ - (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
if (_contentView) {
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}

if (_props->transformOrigin.length() > 0) {
auto newTransform = _props->resolveTransform(layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
}

}

- (BOOL)isJSResponder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,14 @@ - (void)synchronouslyUpdateViewOnUIThread:(ReactTag)reactTag

const auto &newViewProps = static_cast<ViewProps const &>(*newProps);

if (props[@"transform"] &&
!CATransform3DEqualToTransform(
RCTCATransform3DFromTransformMatrix(newViewProps.transform), componentView.layer.transform)) {
componentView.layer.transform = RCTCATransform3DFromTransformMatrix(newViewProps.transform);
if (props[@"transform"]) {
auto layoutMetrics = LayoutMetrics();
layoutMetrics.frame.size.width = componentView.layer.bounds.size.width;
layoutMetrics.frame.size.height = componentView.layer.bounds.size.height;
CATransform3D newTransform = RCTCATransform3DFromTransformMatrix(newViewProps.resolveTransform(layoutMetrics));
if (!CATransform3DEqualToTransform(newTransform, componentView.layer.transform)) {
componentView.layer.transform = newTransform;
}
}
if (props[@"opacity"] && componentView.layer.opacity != (float)newViewProps.opacity) {
componentView.layer.opacity = newViewProps.opacity;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTConvert+Transform.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

@interface RCTConvert (Transform)

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

Choose a reason for hiding this comment

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

For backwords compatibility we should keep the old method signature around too (but have it just call the new method with 0 widht/height and nil origin)


@end
52 changes: 51 additions & 1 deletion packages/react-native/React/Views/RCTConvert+Transform.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ + (CATransform3D)CATransform3DFromMatrix:(id)json
return transform;
}

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

CATransform3D next;

NSArray *offsets = [self getTranslateForTransformOrigin:viewWidth viewHeight:viewHeight transformOrigin: transformOrigin];
CGFloat translateX = [offsets[0] floatValue];
CGFloat translateY = [offsets[1] floatValue];
CGFloat translateZ = [offsets[2] floatValue];
transform = CATransform3DTranslate(transform, translateX, translateY, translateZ);
Comment on lines +71 to +74
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
CGFloat translateX = [offsets[0] floatValue];
CGFloat translateY = [offsets[1] floatValue];
CGFloat translateZ = [offsets[2] floatValue];
transform = CATransform3DTranslate(transform, translateX, translateY, translateZ);
if (offset) {
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,7 +148,50 @@ + (CATransform3D)CATransform3D:(id)json
RCTLogInfo(@"Unsupported transform type for a CATransform3D: %@.", property);
}
}

transform = CATransform3DTranslate(transform, -translateX, -translateY, -translateZ);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
transform = CATransform3DTranslate(transform, -translateX, -translateY, -translateZ);
if (offset) {
transform = CATransform3DTranslate(transform, -translateX, -translateY, -translateZ);
}

return transform;
}

+ (NSArray *)getTranslateForTransformOrigin:(CGFloat)viewWidth viewHeight:(CGFloat)viewHeight transformOrigin
Copy link
Member

Choose a reason for hiding this comment

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

Put this in a static (local) method, no need to have it live on RCTConvert

:(NSString*)transformOrigin {
if (transformOrigin.length == 0 || (viewWidth == 0 && viewHeight == 0)) {
return @[@(0.0), @(0.0), @(0.0)];
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 return nil in this case, and use that to avoid a call to CATransform3DTranslate?

}

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)];
}

@end
2 changes: 2 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,6 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
@property (nonatomic, assign) RCTBubblingEventBlock onGotPointerCapture;
@property (nonatomic, assign) RCTBubblingEventBlock onLostPointerCapture;

@property (nonatomic, strong) id transformProp;
@property (nonatomic, copy) NSString* transformOriginProp;
Comment on lines +138 to +139
Copy link
Member

Choose a reason for hiding this comment

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

Not really convention to suffix these with prop (everything here is a prop). What about rawTransform (preferably with a better type than id) and transformOrigin

@end
6 changes: 6 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,8 @@
#import "RCTLog.h"
#import "RCTViewUtils.h"
#import "UIView+React.h"
#import "RCTConvert+Transform.h"
#import "RCTConvert.h"

RCT_MOCK_DEF(RCTView, RCTContentInsets);
#define RCTContentInsets RCT_MOCK_USE(RCTView, RCTContentInsets)
Expand Down Expand Up @@ -785,6 +787,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.transformOriginProp.length > 0) {
self.layer.transform = [RCTConvert CATransform3D:self.transformProp viewWidth:self.bounds.size.width viewHeight:self.bounds.size.height transformOrigin: self.transformOriginProp];
}
}
}

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

RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
{
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
view.transformProp = json;
view.layer.transform = json ? [RCTConvert CATransform3D:view.transformProp viewWidth:view.bounds.size.width viewHeight:view.bounds.size.height transformOrigin: view.transformOriginProp] : 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;
}

RCT_CUSTOM_VIEW_PROPERTY(transformOrigin, NSString, RCTView)
{
view.transformOriginProp = json;
view.layer.transform = [RCTConvert CATransform3D:view.transformProp viewWidth:view.bounds.size.width viewHeight:view.bounds.size.height transformOrigin: view.transformOriginProp];
// 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;
Comment on lines 232 to 234
Copy link
Member

Choose a reason for hiding this comment

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

No need to duplicate this I think, as transformOrigin couldn't cause this to happen if it hadn't already done so from setTransform?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
* provides support for base view properties such as backgroundColor, opacity, etc.
*/
public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode>
extends ViewManager<T, C> implements BaseViewManagerInterface<T> {
extends ViewManager<T, C> implements BaseViewManagerInterface<T>, View.OnLayoutChangeListener {

private String transformOrigin;
private ReadableArray transform;
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

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

Private variables use m as prefix

Suggested change
private String transformOrigin;
private ReadableArray transform;
private String mTransformOrigin;
private ReadableArray mTransform;

Copy link
Member

Choose a reason for hiding this comment

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

Oh, and these props should live on ReactViewGroup, not ViewManager. A ViewManager is responsible for many views.


private static final int PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX = 2;
private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5);
Expand Down Expand Up @@ -89,6 +92,7 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
view.setRight(0);
view.setElevation(0);
view.setAnimationMatrix(null);
view.removeOnLayoutChangeListener(this);
Copy link
Member

Choose a reason for hiding this comment

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

Please also reset the new fields you've added.


// setShadowColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Expand Down Expand Up @@ -129,6 +133,28 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
return view;
}

@Override
public void onLayoutChange(View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
// Old width and height
int oldWidth = oldRight - oldLeft;
int oldHeight = oldBottom - oldTop;

// Current width and height
int currentWidth = right - left;
int currentHeight = bottom - top;
if ((currentHeight != oldHeight || currentWidth != oldWidth) && this.transformOrigin != null && this.transform != null) {
setTransformProperty((T) v, this.transform, this.transformOrigin);
}
}
Comment on lines +136 to +156
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using onLayoutChange and adding a layout listener, can you override layout (which RN calls to apply the layout)


@Override
@ReactProp(
name = ViewProps.BACKGROUND_COLOR,
Expand All @@ -141,11 +167,23 @@ public void setBackgroundColor(@NonNull T view, int backgroundColor) {
@Override
@ReactProp(name = ViewProps.TRANSFORM)
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
this.transform = matrix;
if (matrix == null) {
resetTransformProperty(view);
} else {
setTransformProperty(view, matrix);
setTransformProperty(view, matrix, this.transformOrigin);
}
}

@Override
@ReactProp(name = ViewProps.TRANSFORM_ORIGIN)
public void setTransformOrigin(@NonNull T view, @Nullable String transformOrigin) {
// we need to recalculate rotation/scale based new dimensions. Attaches same listener, so looks fine.
this.transformOrigin = transformOrigin;
if (this.transform != null) {
setTransformProperty(view, this.transform, this.transformOrigin);
}
view.addOnLayoutChangeListener(this);
}

@Override
Expand Down Expand Up @@ -439,9 +477,9 @@ public void setAccessibilityLiveRegion(@NonNull T view, @Nullable String liveReg
}
}

private static void setTransformProperty(@NonNull View view, ReadableArray transforms) {
private static void setTransformProperty(@NonNull View view, ReadableArray transforms, String transformOrigin) {
sMatrixDecompositionContext.reset();
TransformHelper.processTransform(transforms, sTransformDecompositionArray);
TransformHelper.processTransform(transforms, sTransformDecompositionArray, PixelUtil.toDIPFromPixel(view.getWidth()), PixelUtil.toDIPFromPixel(view.getHeight()), transformOrigin);
MatrixMathHelper.decomposeMatrix(sTransformDecompositionArray, sMatrixDecompositionContext);
view.setTranslationX(
PixelUtil.toPixelFromDIP(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case ViewProps.TRANSFORM:
mViewManager.setTransform(view, (ReadableArray) value);
break;
case ViewProps.TRANSFORM_ORIGIN:
mViewManager.setTransformOrigin(view, (String) value);
break;
case ViewProps.TRANSLATE_X:
mViewManager.setTranslateX(view, value == null ? 0.0f : ((Double) value).floatValue());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public interface BaseViewManagerInterface<T extends View> {

void setTransform(T view, @Nullable ReadableArray matrix);

void setTransformOrigin(T view, @Nullable String transformOrigin);

void setTranslateX(T view, float translateX);

void setTranslateY(T view, float translateY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private static double convertToRadians(ReadableMap transformMap, String key) {
return inRadians ? value : MatrixMathHelper.degreesToRadians(value);
}

public static void processTransform(ReadableArray transforms, double[] result) {
public static void processTransform(ReadableArray transforms, double[] result, float viewWidth, float viewHeight, String transformOrigin) {
Copy link
Member

Choose a reason for hiding this comment

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

Keep the old method too for backwards compat

double[] helperMatrix = sHelperMatrix.get();
MatrixMathHelper.resetIdentityMatrix(result);

Expand All @@ -60,6 +60,10 @@ public static void processTransform(ReadableArray transforms, double[] result) {
return;
Copy link
Member

Choose a reason for hiding this comment

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

Is the transformOrigin already applied in the codepath above?

}

float[] offsets = getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin);
MatrixMathHelper.applyTranslate3D(helperMatrix, offsets[0], offsets[1], offsets[2]);
MatrixMathHelper.multiplyInto(result, result, helperMatrix);
Comment on lines +64 to +65
Copy link
Member

Choose a reason for hiding this comment

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

result is the identity matrix at this point, no multiplication should be required to do this.


for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) {
ReadableMap transform = transforms.getMap(transformIdx);
String transformType = transform.keySetIterator().nextKey();
Expand Down Expand Up @@ -106,5 +110,43 @@ public static void processTransform(ReadableArray transforms, double[] result) {

MatrixMathHelper.multiplyInto(result, result, helperMatrix);
}

MatrixMathHelper.resetIdentityMatrix(helperMatrix);
MatrixMathHelper.applyTranslate3D(helperMatrix, -offsets[0], -offsets[1], -offsets[2]);
MatrixMathHelper.multiplyInto(result, result, helperMatrix);
}

public static float[] getTranslateForTransformOrigin(float viewWidth, float viewHeight, String transformOrigin) {
Copy link
Member

Choose a reason for hiding this comment

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

private

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

float[] origin = {viewCenterX, viewCenterY, 0.0f};

String[] parts = transformOrigin.split(" ");
for (int i = 0; i < parts.length && i < 3; i++) {
String part = parts[i];
if (part.endsWith("%")) {
float val = Float.parseFloat(part.substring(0, part.length() - 1));
origin[i] = (i == 0 ? viewWidth : viewHeight) * val / 100.0f;
} else if (part.equals("top")) {
origin[1] = 0.0f;
} else if (part.equals("bottom")) {
origin[1] = viewHeight;
} else if (part.equals("left")) {
origin[0] = 0.0f;
} else if (part.equals("right")) {
origin[0] = viewWidth;
} else if (part.equals("center")) {
continue;
} else {
origin[i] = Float.parseFloat(part);
}
}

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

return new float[]{newTranslateX, newTranslateY, newTranslateZ};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ public class ViewProps {
public static final String ON_LAYOUT = "onLayout";

public static final String TRANSFORM = "transform";

public static final String TRANSFORM_ORIGIN = "transformOrigin";
public static final String ELEVATION = "elevation";
public static final String SHADOW_COLOR = "shadowColor";
public static final String Z_INDEX = "zIndex";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ class ConcreteViewShadowNode : public ConcreteShadowNode<
}

Transform getTransform() const override {
return BaseShadowNode::getConcreteProps().transform;
auto layoutMetrics = BaseShadowNode::getLayoutMetrics();
return BaseShadowNode::getConcreteProps().resolveTransform(layoutMetrics);
}

private:
Expand Down
Loading