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: ios fabric transform origin #38559

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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,

/**
* 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
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);
intergalacticspacehighway marked this conversation as resolved.
Show resolved Hide resolved
self.layer.allowsEdgeAntialiasing = newViewProps.transform != Transform::Identity();
}

Expand Down Expand Up @@ -397,6 +398,11 @@ - (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 @@ -311,10 +311,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
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ ViewProps::ViewProps(
"transform",
sourceProps.transform,
{})),
transformOrigin(
CoreFeatures::enablePropIteratorSetter ? sourceProps.transformOrigin
: convertRawProp(
context,
rawProps,
"transformOrigin",
sourceProps.transformOrigin,
{})),
backfaceVisibility(
CoreFeatures::enablePropIteratorSetter
? sourceProps.backfaceVisibility
Expand Down Expand Up @@ -412,6 +420,55 @@ BorderMetrics ViewProps::resolveBorderMetrics(
};
}

Transform ViewProps::resolveTransform(
LayoutMetrics const &layoutMetrics) const {
float viewWidth = layoutMetrics.frame.size.width;
float viewHeight = layoutMetrics.frame.size.height;
if (transformOrigin.empty() || (viewWidth == 0 && viewHeight == 0)) {
return transform;
}
std::array<float, 3> translateOffsets = getTranslateForTransformOrigin(viewWidth, viewHeight);
auto newTransform = Transform::Translate(translateOffsets[0], translateOffsets[1], translateOffsets[2]);
newTransform = newTransform * transform;
newTransform = newTransform * Transform::Translate(-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]);
return newTransform;
}

// https://drafts.csswg.org/css-transforms/#transform-origin-property
std::array<float, 3> ViewProps::getTranslateForTransformOrigin(float viewWidth, float viewHeight) const {
float viewCenterX = viewWidth / 2;
float viewCenterY = viewHeight / 2;

std::array<float, 3> origin = {viewCenterX, viewCenterY, 0.0f};
std::istringstream iss(transformOrigin);
std::string part;
for (int i = 0; std::getline(iss, part, ' ') && i < 3; i++) {
auto percentPos = part.find('%');
bool isPercent = percentPos != std::string::npos;
if (isPercent) {
origin[i] = (i == 0 ? viewWidth : viewHeight) * std::stof(part.substr(0, percentPos)) / 100.0f;
} else if (part == "top") {
origin[1] = 0.0f;
} else if (part == "bottom") {
origin[1] = static_cast<float>(viewHeight);
} else if (part == "left") {
origin[0] = 0.0f;
} else if (part == "right") {
origin[0] = static_cast<float>(viewWidth);
} else if (part == "center") {
continue;
} else {
origin[i] = std::stof(part);
}
}

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

return std::array{newTranslateX, newTranslateY, newTranslateZ};
}

bool ViewProps::getClipsContentToBounds() const {
return yogaStyle.overflow() != YGOverflowVisible;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/graphics/Color.h>
#include <react/renderer/graphics/Transform.h>
#include <react/renderer/core/LayoutMetrics.h>

#include <optional>

Expand Down Expand Up @@ -64,6 +65,8 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {

// Transform
Transform transform{};
std::string transformOrigin;

BackfaceVisibility backfaceVisibility{};
bool shouldRasterize{};
std::optional<int> zIndex{};
Expand Down Expand Up @@ -96,6 +99,8 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {
#pragma mark - Convenience Methods

BorderMetrics resolveBorderMetrics(LayoutMetrics const &layoutMetrics) const;
Transform resolveTransform(LayoutMetrics const &layoutMetrics) const;
std::array<float, 3> getTranslateForTransformOrigin(float viewWidth, float viewHeight) const;
bool getClipsContentToBounds() const;

#ifdef ANDROID
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 />;
},
},
];
Loading