Skip to content

Commit f997b81

Browse files
feat(iOS/fabric): percentage support in translate (#43192)
Summary: This PR adds percentage support in translate properties for new arch iOS. Isolating this PR for easier reviews. The approach taken here introduces usage of `ValueUnit` struct for transform operations so it can support `%` in translates and delay the generation of actual transform matrix until view dimensions are known. I have tried to keep the changes minimal and reuse existing APIs, open to changes if there's an alternative approach. ## Changelog: [IOS] [ADDED] - Percentage support in translate in new arch. <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #43192 Test Plan: - Checkout TransformExample.js -> Translate percentage example. - Added a simple test in `processTransform-test.js`. The regex is not perfect (values like 20px%, 20%px will pass, can be improved, let me know!) Related PRs - #43193, #43191 Reviewed By: javache Differential Revision: D56802425 Pulled By: NickGerleman fbshipit-source-id: 978cbbdde004afe1e68ffee9a3c7eb7d16336b46
1 parent 82c6f8a commit f997b81

File tree

14 files changed

+324
-123
lines changed

14 files changed

+324
-123
lines changed

packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ exports[`processTransform validation should throw when passing an invalid angle
3232

3333
exports[`processTransform validation should throw when passing an invalid angle prop 4`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`;
3434

35-
exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be a number: {\\"translateY\\":\\"20deg\\"}"`;
35+
exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be number or a percentage. Passed value: {\\"translateY\\":\\"20deg\\"}."`;
3636

3737
exports[`processTransform validation should throw when passing an invalid value to a number prop 2`] = `"Transform with key of \\"scale\\" must be a number: {\\"scale\\":{\\"x\\":10,\\"y\\":10}}"`;
3838

packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ describe('processTransform', () => {
3434
);
3535
});
3636

37+
it('should accept a percentage translate transform', () => {
38+
processTransform([{translateY: '20%'}, {translateX: '10%'}]);
39+
processTransform('translateX(10%)');
40+
});
41+
3742
it('should throw on object with multiple properties', () => {
3843
expect(() =>
3944
processTransform([{scale: 0.5, translateY: 10}]),

packages/react-native/Libraries/StyleSheet/processTransform.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ const _getKeyAndValueFromCSSTransform: (
6868
| $TEMPORARY$string<'translateX'>
6969
| $TEMPORARY$string<'translateY'>,
7070
args: string,
71-
) => {key: string, value?: number[] | number | string} = (key, args) => {
72-
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g);
71+
) => {key: string, value?: Array<string | number> | number | string} = (
72+
key,
73+
args,
74+
) => {
75+
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+|%)?/g);
7376

7477
switch (key) {
7578
case 'matrix':
@@ -88,7 +91,11 @@ const _getKeyAndValueFromCSSTransform: (
8891
missingUnitOfMeasurement = true;
8992
}
9093

91-
parsedArgs.push(value);
94+
if (unitOfMeasurement === '%') {
95+
parsedArgs.push(`${value}%`);
96+
} else {
97+
parsedArgs.push(value);
98+
}
9299
}
93100

94101
if (__DEV__) {
@@ -256,6 +263,14 @@ function _validateTransform(
256263
break;
257264
case 'translateX':
258265
case 'translateY':
266+
invariant(
267+
typeof value === 'number' ||
268+
(typeof value === 'string' && value.endsWith('%')),
269+
'Transform with key of "%s" must be number or a percentage. Passed value: %s.',
270+
key,
271+
stringifySafe(transformation),
272+
);
273+
break;
259274
case 'scale':
260275
case 'scaleX':
261276
case 'scaleY':

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
421421
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
422422
}
423423

424-
if (_props->transformOrigin.isSet()) {
424+
if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) &&
425+
layoutMetrics.frame.size != oldLayoutMetrics.frame.size) {
425426
auto newTransform = _props->resolveTransform(layoutMetrics);
426427
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
427428
}

packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,12 +1112,14 @@ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView(
11121112
// Animate opacity or scale/transform
11131113
PropsParserContext propsParserContext{
11141114
finalView.surfaceId, *contextContainer_};
1115+
const auto& finalViewSize = finalView.layoutMetrics.frame.size;
11151116
mutatedShadowView.props = interpolateProps(
11161117
componentDescriptor,
11171118
propsParserContext,
11181119
progress,
11191120
startingView.props,
1120-
finalView.props);
1121+
finalView.props,
1122+
finalViewSize);
11211123

11221124
react_native_assert(mutatedShadowView.props != nullptr);
11231125
if (mutatedShadowView.props == nullptr) {
@@ -1626,7 +1628,8 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps(
16261628
const PropsParserContext& context,
16271629
Float animationProgress,
16281630
const Props::Shared& props,
1629-
const Props::Shared& newProps) const {
1631+
const Props::Shared& newProps,
1632+
const Size& size) const {
16301633
#ifdef ANDROID
16311634
// On Android only, the merged props should have the same RawProps as the
16321635
// final props struct
@@ -1643,7 +1646,7 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps(
16431646
if (componentDescriptor.getTraits().check(
16441647
ShadowNodeTraits::Trait::ViewKind)) {
16451648
interpolateViewProps(
1646-
animationProgress, props, newProps, interpolatedPropsShared);
1649+
animationProgress, props, newProps, interpolatedPropsShared, size);
16471650
}
16481651

16491652
return interpolatedPropsShared;

packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate,
179179
const PropsParserContext& context,
180180
Float animationProgress,
181181
const Props::Shared& props,
182-
const Props::Shared& newProps) const;
182+
const Props::Shared& newProps,
183+
const Size& size) const;
183184
};
184185

185186
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -458,21 +458,36 @@ BorderMetrics BaseViewProps::resolveBorderMetrics(
458458

459459
Transform BaseViewProps::resolveTransform(
460460
const LayoutMetrics& layoutMetrics) const {
461-
float viewWidth = layoutMetrics.frame.size.width;
462-
float viewHeight = layoutMetrics.frame.size.height;
463-
if (!transformOrigin.isSet() || (viewWidth == 0 && viewHeight == 0)) {
464-
return transform;
461+
const auto& frameSize = layoutMetrics.frame.size;
462+
auto transformMatrix = Transform{};
463+
if (frameSize.width == 0 && frameSize.height == 0) {
464+
return transformMatrix;
465465
}
466-
std::array<float, 3> translateOffsets =
467-
getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin);
468-
auto newTransform = Transform::Translate(
469-
translateOffsets[0], translateOffsets[1], translateOffsets[2]);
470-
newTransform = newTransform * transform;
471-
newTransform =
472-
newTransform *
473-
Transform::Translate(
474-
-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]);
475-
return newTransform;
466+
467+
// transform is matrix
468+
if (transform.operations.size() == 1 &&
469+
transform.operations[0].type == TransformOperationType::Arbitrary) {
470+
transformMatrix = transform;
471+
} else {
472+
for (const auto& operation : transform.operations) {
473+
transformMatrix = transformMatrix *
474+
Transform::FromTransformOperation(
475+
operation, layoutMetrics.frame.size);
476+
}
477+
}
478+
479+
if (transformOrigin.isSet()) {
480+
std::array<float, 3> translateOffsets = getTranslateForTransformOrigin(
481+
frameSize.width, frameSize.height, transformOrigin);
482+
transformMatrix =
483+
Transform::Translate(
484+
translateOffsets[0], translateOffsets[1], translateOffsets[2]) *
485+
transformMatrix *
486+
Transform::Translate(
487+
-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]);
488+
}
489+
490+
return transformMatrix;
476491
}
477492

478493
bool BaseViewProps::getClipsContentToBounds() const {

packages/react-native/ReactCommon/react/renderer/components/view/ViewPropsInterpolation.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ static inline void interpolateViewProps(
2121
Float animationProgress,
2222
const Props::Shared& oldPropsShared,
2323
const Props::Shared& newPropsShared,
24-
Props::Shared& interpolatedPropsShared) {
24+
Props::Shared& interpolatedPropsShared,
25+
const Size& size) {
2526
const ViewProps* oldViewProps =
2627
static_cast<const ViewProps*>(oldPropsShared.get());
2728
const ViewProps* newViewProps =
@@ -31,9 +32,11 @@ static inline void interpolateViewProps(
3132

3233
interpolatedProps->opacity = oldViewProps->opacity +
3334
(newViewProps->opacity - oldViewProps->opacity) * animationProgress;
34-
3535
interpolatedProps->transform = Transform::Interpolate(
36-
animationProgress, oldViewProps->transform, newViewProps->transform);
36+
animationProgress,
37+
oldViewProps->transform,
38+
newViewProps->transform,
39+
size);
3740

3841
// Android uses RawProps, not props, to update props on the platform...
3942
// Since interpolated props don't interpolate at all using RawProps, we need

packages/react-native/ReactCommon/react/renderer/components/view/conversions.h

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,35 @@ inline Float toRadians(
480480
return static_cast<Float>(num); // assume suffix is "rad"
481481
}
482482

483+
inline void fromRawValue(
484+
const PropsParserContext& /*context*/,
485+
const RawValue& value,
486+
ValueUnit& result) {
487+
react_native_expect(value.hasType<RawValue>());
488+
ValueUnit valueUnit;
489+
490+
if (value.hasType<Float>()) {
491+
auto valueFloat = (float)value;
492+
if (std::isfinite(valueFloat)) {
493+
valueUnit = ValueUnit(valueFloat, UnitType::Point);
494+
} else {
495+
valueUnit = ValueUnit(0.0f, UnitType::Undefined);
496+
}
497+
} else if (value.hasType<std::string>()) {
498+
const auto stringValue = (std::string)value;
499+
500+
if (stringValue.back() == '%') {
501+
auto tryValue = folly::tryTo<float>(
502+
std::string_view(stringValue).substr(0, stringValue.length() - 1));
503+
if (tryValue.hasValue()) {
504+
valueUnit = ValueUnit(tryValue.value(), UnitType::Percent);
505+
}
506+
}
507+
}
508+
509+
result = valueUnit;
510+
}
511+
483512
inline void fromRawValue(
484513
const PropsParserContext& context,
485514
const RawValue& value,
@@ -504,6 +533,8 @@ inline void fromRawValue(
504533
auto pair = configurationPair.begin();
505534
auto operation = pair->first;
506535
auto& parameters = pair->second;
536+
auto Zero = ValueUnit(0, UnitType::Point);
537+
auto One = ValueUnit(1, UnitType::Point);
507538

508539
if (operation == "matrix") {
509540
react_native_expect(parameters.hasType<std::vector<Float>>());
@@ -513,84 +544,90 @@ inline void fromRawValue(
513544
for (auto number : numbers) {
514545
transformMatrix.matrix[i++] = number;
515546
}
516-
transformMatrix.operations.push_back(
517-
TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0});
547+
transformMatrix.operations.push_back(TransformOperation{
548+
TransformOperationType::Arbitrary, Zero, Zero, Zero});
518549
} else if (operation == "perspective") {
519-
transformMatrix =
520-
transformMatrix * Transform::Perspective((Float)parameters);
550+
transformMatrix.operations.push_back(TransformOperation{
551+
TransformOperationType::Perspective,
552+
ValueUnit((Float)parameters, UnitType::Point),
553+
Zero,
554+
Zero});
521555
} else if (operation == "rotateX") {
522-
transformMatrix = transformMatrix *
523-
Transform::Rotate(toRadians(parameters, 0.0f), 0, 0);
556+
transformMatrix.operations.push_back(TransformOperation{
557+
TransformOperationType::Rotate,
558+
ValueUnit(toRadians(parameters, 0.0f), UnitType::Point),
559+
Zero,
560+
Zero});
524561
} else if (operation == "rotateY") {
525-
transformMatrix = transformMatrix *
526-
Transform::Rotate(0, toRadians(parameters, 0.0f), 0);
562+
transformMatrix.operations.push_back(TransformOperation{
563+
TransformOperationType::Rotate,
564+
Zero,
565+
ValueUnit(toRadians(parameters, 0.0f), UnitType::Point),
566+
Zero});
527567
} else if (operation == "rotateZ" || operation == "rotate") {
528-
transformMatrix = transformMatrix *
529-
Transform::Rotate(0, 0, toRadians(parameters, 0.0f));
568+
transformMatrix.operations.push_back(TransformOperation{
569+
TransformOperationType::Rotate,
570+
Zero,
571+
Zero,
572+
ValueUnit(toRadians(parameters, 0.0f), UnitType::Point)});
530573
} else if (operation == "scale") {
531-
auto number = (Float)parameters;
532-
transformMatrix =
533-
transformMatrix * Transform::Scale(number, number, number);
574+
auto number = ValueUnit((Float)parameters, UnitType::Point);
575+
transformMatrix.operations.push_back(TransformOperation{
576+
TransformOperationType::Scale, number, number, number});
534577
} else if (operation == "scaleX") {
535-
transformMatrix =
536-
transformMatrix * Transform::Scale((Float)parameters, 1, 1);
578+
transformMatrix.operations.push_back(TransformOperation{
579+
TransformOperationType::Scale,
580+
ValueUnit((Float)parameters, UnitType::Point),
581+
One,
582+
One});
537583
} else if (operation == "scaleY") {
538-
transformMatrix =
539-
transformMatrix * Transform::Scale(1, (Float)parameters, 1);
584+
transformMatrix.operations.push_back(TransformOperation{
585+
TransformOperationType::Scale,
586+
One,
587+
ValueUnit((Float)parameters, UnitType::Point),
588+
One});
540589
} else if (operation == "scaleZ") {
541-
transformMatrix =
542-
transformMatrix * Transform::Scale(1, 1, (Float)parameters);
590+
transformMatrix.operations.push_back(TransformOperation{
591+
TransformOperationType::Scale,
592+
One,
593+
One,
594+
ValueUnit((Float)parameters, UnitType::Point)});
543595
} else if (operation == "translate") {
544-
auto numbers = (std::vector<Float>)parameters;
545-
transformMatrix = transformMatrix *
546-
Transform::Translate(numbers.at(0), numbers.at(1), 0);
596+
auto numbers = (std::vector<RawValue>)parameters;
597+
ValueUnit valueX;
598+
fromRawValue(context, numbers.at(0), valueX);
599+
ValueUnit valueY;
600+
fromRawValue(context, numbers.at(1), valueY);
601+
transformMatrix.operations.push_back(TransformOperation{
602+
TransformOperationType::Translate, valueX, valueY, Zero});
547603
} else if (operation == "translateX") {
548-
transformMatrix =
549-
transformMatrix * Transform::Translate((Float)parameters, 0, 0);
604+
ValueUnit valueX;
605+
fromRawValue(context, parameters, valueX);
606+
transformMatrix.operations.push_back(TransformOperation{
607+
TransformOperationType::Translate, valueX, Zero, Zero});
550608
} else if (operation == "translateY") {
551-
transformMatrix =
552-
transformMatrix * Transform::Translate(0, (Float)parameters, 0);
609+
ValueUnit valueY;
610+
fromRawValue(context, parameters, valueY);
611+
transformMatrix.operations.push_back(TransformOperation{
612+
TransformOperationType::Translate, Zero, valueY, Zero});
553613
} else if (operation == "skewX") {
554-
transformMatrix =
555-
transformMatrix * Transform::Skew(toRadians(parameters, 0.0f), 0);
614+
transformMatrix.operations.push_back(TransformOperation{
615+
TransformOperationType::Skew,
616+
ValueUnit(toRadians(parameters, 0.0f), UnitType::Point),
617+
Zero,
618+
Zero});
556619
} else if (operation == "skewY") {
557-
transformMatrix =
558-
transformMatrix * Transform::Skew(0, toRadians(parameters, 0.0f));
620+
transformMatrix.operations.push_back(TransformOperation{
621+
TransformOperationType::Skew,
622+
Zero,
623+
ValueUnit(toRadians(parameters, 0.0f), UnitType::Point),
624+
Zero});
559625
}
560626
}
561627

562628
result = transformMatrix;
563629
}
564630

565-
inline void fromRawValue(
566-
const PropsParserContext& /*context*/,
567-
const RawValue& value,
568-
ValueUnit& result) {
569-
react_native_expect(value.hasType<RawValue>());
570-
ValueUnit valueUnit;
571-
572-
if (value.hasType<Float>()) {
573-
auto valueFloat = (float)value;
574-
if (std::isfinite(valueFloat)) {
575-
valueUnit = ValueUnit(valueFloat, UnitType::Point);
576-
} else {
577-
valueUnit = ValueUnit(0.0f, UnitType::Undefined);
578-
}
579-
} else if (value.hasType<std::string>()) {
580-
const auto stringValue = (std::string)value;
581-
582-
if (stringValue.back() == '%') {
583-
auto tryValue = folly::tryTo<float>(
584-
std::string_view(stringValue).substr(0, stringValue.length() - 1));
585-
if (tryValue.hasValue()) {
586-
valueUnit = ValueUnit(tryValue.value(), UnitType::Percent);
587-
}
588-
}
589-
}
590-
591-
result = valueUnit;
592-
}
593-
594631
inline void fromRawValue(
595632
const PropsParserContext& context,
596633
const RawValue& value,

0 commit comments

Comments
 (0)