From f84513deb945ba0e86bb731d32ff919cdb066c61 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Fri, 1 Sep 2023 06:18:36 -0700 Subject: [PATCH] Implement transform-origin for old arch iOS (#38626) Summary: Adds transform-origin for old arch iOS See also intergalacticspacehighway's work for iOS Fabric and Android:- - https://github.com/facebook/react-native/pull/38559 - https://github.com/facebook/react-native/pull/38558 ## Changelog: [IOS] [ADDED] - Added support for transform-origin on old arch Pull Request resolved: https://github.com/facebook/react-native/pull/38626 Test Plan: See RN tester Differential Revision: D48528353 Pulled By: javache fbshipit-source-id: 0189f374c9556b6593b3d72d7503b9cf166378c2 --- .../React/Views/RCTTransformOrigin.h | 15 +++++ .../react-native/React/Views/RCTViewManager.m | 17 +++-- .../react-native/React/Views/UIView+React.h | 8 +++ .../react-native/React/Views/UIView+React.m | 66 +++++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 packages/react-native/React/Views/RCTTransformOrigin.h diff --git a/packages/react-native/React/Views/RCTTransformOrigin.h b/packages/react-native/React/Views/RCTTransformOrigin.h new file mode 100644 index 00000000000000..1823fd91f1ebfe --- /dev/null +++ b/packages/react-native/React/Views/RCTTransformOrigin.h @@ -0,0 +1,15 @@ +/* + * 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. + */ + +#import +#import + +typedef struct { + YGValue x; + YGValue y; + CGFloat z; +} RCTTransformOrigin; diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index a88064337cfb73..1c57ffc0a526dd 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -15,6 +15,7 @@ #import "RCTConvert.h" #import "RCTLog.h" #import "RCTShadowView.h" +#import "RCTTransformOrigin.h" #import "RCTUIManager.h" #import "RCTUIManagerUtils.h" #import "RCTUtils.h" @@ -121,6 +122,13 @@ @implementation RCTConvert (UIAccessibilityTraits) UIAccessibilityTraitNone, unsignedLongLongValue) ++ (RCTTransformOrigin)RCTTransformOrigin:(id)json +{ + RCTTransformOrigin transformOrigin = { + [RCTConvert YGValue:json[0]], [RCTConvert YGValue:json[1]], [RCTConvert CGFloat:json[2]]}; + return transformOrigin; +} + @end @implementation RCTViewManager @@ -216,13 +224,8 @@ - (RCTShadowView *)shadowView view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale; } -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; -} +RCT_REMAP_VIEW_PROPERTY(transform, reactTransform, CATransform3D) +RCT_REMAP_VIEW_PROPERTY(transformOrigin, reactTransformOrigin, RCTTransformOrigin) RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView) { diff --git a/packages/react-native/React/Views/UIView+React.h b/packages/react-native/React/Views/UIView+React.h index d378a8320ba1d3..46bd1660aae98d 100644 --- a/packages/react-native/React/Views/UIView+React.h +++ b/packages/react-native/React/Views/UIView+React.h @@ -8,6 +8,7 @@ #import #import +#import #import @class RCTShadowView; @@ -104,6 +105,13 @@ @property (nonatomic, readonly) UIEdgeInsets reactCompoundInsets; @property (nonatomic, readonly) CGRect reactContentFrame; +/** + * 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, assign) RCTTransformOrigin reactTransformOrigin; + /** * The (sub)view which represents this view in terms of accessibility. * ViewManager will apply all accessibility properties directly to this view. diff --git a/packages/react-native/React/Views/UIView+React.m b/packages/react-native/React/Views/UIView+React.m index 7c6c71829ff69d..8e3f4961306368 100644 --- a/packages/react-native/React/Views/UIView+React.m +++ b/packages/react-native/React/Views/UIView+React.m @@ -203,6 +203,72 @@ - (void)reactSetFrame:(CGRect)frame self.center = position; self.bounds = bounds; + + updateTransform(self); +} + +#pragma mark - Transforms + +- (CATransform3D)reactTransform +{ + id obj = objc_getAssociatedObject(self, _cmd); + return obj != nil ? [obj CATransform3DValue] : CATransform3DIdentity; +} + +- (void)setReactTransform:(CATransform3D)reactTransform +{ + objc_setAssociatedObject(self, @selector(reactTransform), @(reactTransform), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + updateTransform(self); +} + +- (RCTTransformOrigin)reactTransformOrigin +{ + id obj = objc_getAssociatedObject(self, _cmd); + if (obj != nil) { + RCTTransformOrigin transformOrigin; + [obj getValue:&transformOrigin]; + return transformOrigin; + } else { + return (RCTTransformOrigin){(YGValue){50, YGUnitPercent}, (YGValue){50, YGUnitPercent}, 0}; + } +} + +- (void)setReactTransformOrigin:(RCTTransformOrigin)reactTransformOrigin +{ + id obj = [NSValue value:&reactTransformOrigin withObjCType:@encode(RCTTransformOrigin)]; + objc_setAssociatedObject(self, @selector(reactTransformOrigin), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + updateTransform(self); +} + +static void updateTransform(UIView *view) +{ + CGSize size = view.bounds.size; + RCTTransformOrigin transformOrigin = view.reactTransformOrigin; + + CGFloat anchorPointX = 0; + CGFloat anchorPointY = 0; + CGFloat anchorPointZ = 0; + + if (transformOrigin.x.unit == YGUnitPoint) { + anchorPointX = transformOrigin.x.value - size.width * 0.5; + } else if (transformOrigin.x.unit == YGUnitPercent) { + anchorPointX = (transformOrigin.x.value * 0.01 - 0.5) * size.width; + } + + if (transformOrigin.y.unit == YGUnitPoint) { + anchorPointY = transformOrigin.y.value - size.height * 0.5; + } else if (transformOrigin.y.unit == YGUnitPercent) { + anchorPointY = (transformOrigin.y.value * 0.01 - 0.5) * size.height; + } + + anchorPointZ = transformOrigin.z; + + CATransform3D transform = CATransform3DMakeTranslation(anchorPointX, anchorPointY, anchorPointZ); + transform = CATransform3DConcat(view.reactTransform, transform); + transform = CATransform3DTranslate(transform, -anchorPointX, -anchorPointY, -anchorPointZ); + + view.layer.transform = transform; + view.layer.allowsEdgeAntialiasing = transform.m12 != 0.0f || transform.m21 != 0.0f || transform.m34 != 0.0f; } - (UIViewController *)reactViewController