diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap b/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap index dc8d045189ea5e..119c7344a130ba 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap +++ b/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap @@ -32,7 +32,7 @@ exports[`processTransform validation should throw when passing an invalid angle 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\\"}"`; -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\\"}"`; +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\\"}."`; 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}}"`; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js index 66ce708c033897..fe7c5d2851505f 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js @@ -34,6 +34,11 @@ describe('processTransform', () => { ); }); + it('should accept a percentage translate transform', () => { + processTransform([{translateY: '20%'}, {translateX: '10%'}]); + processTransform('translateX(10%)'); + }); + it('should throw on object with multiple properties', () => { expect(() => processTransform([{scale: 0.5, translateY: 10}]), diff --git a/packages/react-native/Libraries/StyleSheet/processTransform.js b/packages/react-native/Libraries/StyleSheet/processTransform.js index a703ac47161462..6310a4b5c77550 100644 --- a/packages/react-native/Libraries/StyleSheet/processTransform.js +++ b/packages/react-native/Libraries/StyleSheet/processTransform.js @@ -68,8 +68,11 @@ const _getKeyAndValueFromCSSTransform: ( | $TEMPORARY$string<'translateX'> | $TEMPORARY$string<'translateY'>, args: string, -) => {key: string, value?: number[] | number | string} = (key, args) => { - const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g); +) => {key: string, value?: Array | number | string} = ( + key, + args, +) => { + const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+|%)?/g); switch (key) { case 'matrix': @@ -88,7 +91,11 @@ const _getKeyAndValueFromCSSTransform: ( missingUnitOfMeasurement = true; } - parsedArgs.push(value); + if (unitOfMeasurement === '%') { + parsedArgs.push(`${value}%`); + } else { + parsedArgs.push(value); + } } if (__DEV__) { @@ -256,6 +263,14 @@ function _validateTransform( break; case 'translateX': case 'translateY': + invariant( + typeof value === 'number' || + (typeof value === 'string' && value.endsWith('%')), + 'Transform with key of "%s" must be number or a percentage. Passed value: %s.', + key, + stringifySafe(transformation), + ); + break; case 'scale': case 'scaleX': case 'scaleY': diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index aeaad4acedb31a..7d765d1433cf95 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -415,7 +415,7 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics _contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); } - if (_props->transformOrigin.isSet()) { + if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) && layoutMetrics.frame.size != oldLayoutMetrics.frame.size) { auto newTransform = _props->resolveTransform(layoutMetrics); self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform); } diff --git a/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp b/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp index 4c128b5db8d1f8..4606ae34a7818c 100644 --- a/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp @@ -1113,12 +1113,15 @@ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView( // Animate opacity or scale/transform PropsParserContext propsParserContext{ finalView.surfaceId, *contextContainer_}; + auto finalViewSize = Size{finalView.layoutMetrics.frame.size.width, finalView.layoutMetrics.frame.size.height}; mutatedShadowView.props = interpolateProps( componentDescriptor, propsParserContext, progress, startingView.props, - finalView.props); + finalView.props, + finalViewSize + ); react_native_assert(mutatedShadowView.props != nullptr); if (mutatedShadowView.props == nullptr) { @@ -1627,7 +1630,8 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps( const PropsParserContext& context, Float animationProgress, const Props::Shared& props, - const Props::Shared& newProps) const { + const Props::Shared& newProps, + const Size& size) const { #ifdef ANDROID // On Android only, the merged props should have the same RawProps as the // final props struct @@ -1640,11 +1644,11 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps( Props::Shared interpolatedPropsShared = componentDescriptor.cloneProps(context, newProps, {}); #endif - + if (componentDescriptor.getTraits().check( ShadowNodeTraits::Trait::ViewKind)) { interpolateViewProps( - animationProgress, props, newProps, interpolatedPropsShared); + animationProgress, props, newProps, interpolatedPropsShared, size); } return interpolatedPropsShared; diff --git a/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h b/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h index 04b5acc45ed915..82342767c4ce50 100644 --- a/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h +++ b/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h @@ -179,7 +179,8 @@ class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate, const PropsParserContext& context, Float animationProgress, const Props::Shared& props, - const Props::Shared& newProps) const; + const Props::Shared& newProps, + const Size& size) const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 0db0b084263c00..f44988591f9a5c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -401,19 +401,33 @@ Transform BaseViewProps::resolveTransform( const LayoutMetrics& layoutMetrics) const { float viewWidth = layoutMetrics.frame.size.width; float viewHeight = layoutMetrics.frame.size.height; - if (!transformOrigin.isSet() || (viewWidth == 0 && viewHeight == 0)) { - return transform; + auto transformMatrix = Transform{}; + + if (viewWidth == 0 && viewHeight == 0){ + return transformMatrix; } - std::array translateOffsets = - getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin); - auto newTransform = Transform::Translate( - translateOffsets[0], translateOffsets[1], translateOffsets[2]); - newTransform = newTransform * transform; - newTransform = - newTransform * + + // transform is matrix + if (transform.operations.size() == 1 && transform.operations[0].type == TransformOperationType::Arbitrary) { + transformMatrix = transform; + } else { + auto viewSize = Size{viewWidth, viewHeight}; + for (size_t i = 0; i < transform.operations.size(); i++) { + transformMatrix = transformMatrix * transform.FromTransformOperation(transform.operations[i], viewSize); + } + } + + if (transformOrigin.isSet()) { + std::array translateOffsets = + getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin); + transformMatrix = Transform::Translate( - -translateOffsets[0], -translateOffsets[1], -translateOffsets[2]); - return newTransform; + translateOffsets[0], translateOffsets[1], translateOffsets[2]) + * transformMatrix + * Transform::Translate(-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]); + } + + return transformMatrix; } bool BaseViewProps::getClipsContentToBounds() const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewPropsInterpolation.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewPropsInterpolation.h index 2e95558c493273..e60064a637d9de 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewPropsInterpolation.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewPropsInterpolation.h @@ -21,7 +21,8 @@ static inline void interpolateViewProps( Float animationProgress, const Props::Shared& oldPropsShared, const Props::Shared& newPropsShared, - Props::Shared& interpolatedPropsShared) { + Props::Shared& interpolatedPropsShared, + const Size& size) { const ViewProps* oldViewProps = static_cast(oldPropsShared.get()); const ViewProps* newViewProps = @@ -31,9 +32,8 @@ static inline void interpolateViewProps( interpolatedProps->opacity = oldViewProps->opacity + (newViewProps->opacity - oldViewProps->opacity) * animationProgress; - interpolatedProps->transform = Transform::Interpolate( - animationProgress, oldViewProps->transform, newViewProps->transform); + animationProgress, oldViewProps->transform, newViewProps->transform, size); // Android uses RawProps, not props, to update props on the platform... // Since interpolated props don't interpolate at all using RawProps, we need diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 0f9f159c188164..e71c2455d78e8e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -503,6 +503,8 @@ inline void fromRawValue( auto pair = configurationPair.begin(); auto operation = pair->first; auto& parameters = pair->second; + auto Zero = ValueUnit(0, UnitType::Point); + auto One = ValueUnit(1, UnitType::Point); if (operation == "matrix") { react_native_expect(parameters.hasType>()); @@ -513,48 +515,52 @@ inline void fromRawValue( transformMatrix.matrix[i++] = number; } transformMatrix.operations.push_back( - TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}); + TransformOperation{TransformOperationType::Arbitrary, Zero, Zero, Zero}); } else if (operation == "perspective") { - transformMatrix = - transformMatrix * Transform::Perspective((Float)parameters); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Perspective, ValueUnit((Float)parameters, UnitType::Point), Zero, Zero}); } else if (operation == "rotateX") { - transformMatrix = transformMatrix * - Transform::Rotate(toRadians(parameters, 0.0f), 0, 0); + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Rotate, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero, Zero}); } else if (operation == "rotateY") { - transformMatrix = transformMatrix * - Transform::Rotate(0, toRadians(parameters, 0.0f), 0); + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Rotate, Zero, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero}); } else if (operation == "rotateZ" || operation == "rotate") { - transformMatrix = transformMatrix * - Transform::Rotate(0, 0, toRadians(parameters, 0.0f)); + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Rotate, Zero, Zero, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point)}); } else if (operation == "scale") { - auto number = (Float)parameters; - transformMatrix = - transformMatrix * Transform::Scale(number, number, number); + auto number = ValueUnit((Float)parameters, UnitType::Point); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, number, number, number}); } else if (operation == "scaleX") { - transformMatrix = - transformMatrix * Transform::Scale((Float)parameters, 1, 1); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, ValueUnit((Float)parameters, UnitType::Point), One, One}); } else if (operation == "scaleY") { - transformMatrix = - transformMatrix * Transform::Scale(1, (Float)parameters, 1); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, One, ValueUnit((Float)parameters, UnitType::Point), One}); } else if (operation == "scaleZ") { - transformMatrix = - transformMatrix * Transform::Scale(1, 1, (Float)parameters); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, One, One, ValueUnit((Float)parameters, UnitType::Point)}); } else if (operation == "translate") { - auto numbers = (std::vector)parameters; - transformMatrix = transformMatrix * - Transform::Translate(numbers.at(0), numbers.at(1), 0); + auto numbers = (std::vector)parameters; + auto valueX = ValueUnit::getValueUnitFromRawValue(numbers.at(0)); + auto valueY = ValueUnit::getValueUnitFromRawValue(numbers.at(1)); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, valueX, valueY, Zero}); } else if (operation == "translateX") { - transformMatrix = - transformMatrix * Transform::Translate((Float)parameters, 0, 0); + auto value = ValueUnit::getValueUnitFromRawValue(parameters); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, value, Zero, Zero}); } else if (operation == "translateY") { - transformMatrix = - transformMatrix * Transform::Translate(0, (Float)parameters, 0); + auto value = ValueUnit::getValueUnitFromRawValue(parameters); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, Zero, value, Zero}); } else if (operation == "skewX") { - transformMatrix = - transformMatrix * Transform::Skew(toRadians(parameters, 0.0f), 0); + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Skew, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero, Zero}); } else if (operation == "skewY") { - transformMatrix = - transformMatrix * Transform::Skew(0, toRadians(parameters, 0.0f)); + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Skew, Zero, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero}); } } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp index 3cd227e0292df3..9a598f18d0c2a4 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp @@ -41,8 +41,9 @@ Transform Transform::HorizontalInversion() { Transform Transform::Perspective(Float perspective) { auto transform = Transform{}; + auto Zero = ValueUnit(0, UnitType::Point); transform.operations.push_back(TransformOperation{ - TransformOperationType::Perspective, perspective, 0, 0}); + TransformOperationType::Perspective, ValueUnit(perspective, UnitType::Point), Zero, Zero}); transform.matrix[11] = -1 / perspective; return transform; } @@ -54,7 +55,7 @@ Transform Transform::Scale(Float x, Float y, Float z) { Float zprime = isZero(z) ? 0 : z; if (xprime != 1 || yprime != 1 || zprime != 1) { transform.operations.push_back(TransformOperation{ - TransformOperationType::Scale, xprime, yprime, zprime}); + TransformOperationType::Scale, ValueUnit(xprime, UnitType::Point), ValueUnit(yprime, UnitType::Point), ValueUnit(zprime, UnitType::Point)}); transform.matrix[0] = xprime; transform.matrix[5] = yprime; transform.matrix[10] = zprime; @@ -69,7 +70,7 @@ Transform Transform::Translate(Float x, Float y, Float z) { Float zprime = isZero(z) ? 0 : z; if (xprime != 0 || yprime != 0 || zprime != 0) { transform.operations.push_back(TransformOperation{ - TransformOperationType::Translate, xprime, yprime, zprime}); + TransformOperationType::Translate, ValueUnit(xprime, UnitType::Point), ValueUnit(yprime, UnitType::Point), ValueUnit(zprime, UnitType::Point)}); transform.matrix[12] = xprime; transform.matrix[13] = yprime; transform.matrix[14] = zprime; @@ -82,7 +83,7 @@ Transform Transform::Skew(Float x, Float y) { Float xprime = isZero(x) ? 0 : x; Float yprime = isZero(y) ? 0 : y; transform.operations.push_back( - TransformOperation{TransformOperationType::Skew, xprime, yprime, 0}); + TransformOperation{TransformOperationType::Skew, ValueUnit(xprime, UnitType::Point), ValueUnit(yprime, UnitType::Point), ValueUnit(0, UnitType::Point)}); transform.matrix[4] = std::tan(xprime); transform.matrix[1] = std::tan(yprime); return transform; @@ -91,8 +92,9 @@ Transform Transform::Skew(Float x, Float y) { Transform Transform::RotateX(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { + auto Zero = ValueUnit(0, UnitType::Point); transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, radians, 0, 0}); + TransformOperation{TransformOperationType::Rotate, ValueUnit(radians, UnitType::Point), Zero, Zero}); transform.matrix[5] = std::cos(radians); transform.matrix[6] = std::sin(radians); transform.matrix[9] = -std::sin(radians); @@ -104,8 +106,9 @@ Transform Transform::RotateX(Float radians) { Transform Transform::RotateY(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { + auto Zero = ValueUnit(0, UnitType::Point); transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, 0, radians, 0}); + TransformOperation{TransformOperationType::Rotate, Zero, ValueUnit(radians, UnitType::Point), Zero}); transform.matrix[0] = std::cos(radians); transform.matrix[2] = -std::sin(radians); transform.matrix[8] = std::sin(radians); @@ -117,8 +120,9 @@ Transform Transform::RotateY(Float radians) { Transform Transform::RotateZ(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { + auto Zero = ValueUnit(0, UnitType::Point); transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, 0, 0, radians}); + TransformOperation{TransformOperationType::Rotate, Zero, Zero, ValueUnit(radians, UnitType::Point)}); transform.matrix[0] = std::cos(radians); transform.matrix[1] = std::sin(radians); transform.matrix[4] = -std::sin(radians); @@ -130,7 +134,7 @@ Transform Transform::RotateZ(Float radians) { Transform Transform::Rotate(Float x, Float y, Float z) { auto transform = Transform{}; transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, x, y, z}); + TransformOperation{TransformOperationType::Rotate, ValueUnit(x, UnitType::Point), ValueUnit(y, UnitType::Point), ValueUnit(z, UnitType::Point)}); if (!isZero(x)) { transform = transform * Transform::RotateX(x); } @@ -144,24 +148,27 @@ Transform Transform::Rotate(Float x, Float y, Float z) { } Transform Transform::FromTransformOperation( - TransformOperation transformOperation) { + TransformOperation transformOperation, const Size& size) { if (transformOperation.type == TransformOperationType::Perspective) { - return Transform::Perspective(transformOperation.x); + return Transform::Perspective(transformOperation.x.resolve(0)); } if (transformOperation.type == TransformOperationType::Scale) { return Transform::Scale( - transformOperation.x, transformOperation.y, transformOperation.z); + transformOperation.x.resolve(0), transformOperation.y.resolve(0), transformOperation.z.resolve(0)); } if (transformOperation.type == TransformOperationType::Translate) { + auto translateX = transformOperation.x.resolve(size.width); + auto translateY = transformOperation.y.resolve(size.height); + return Transform::Translate( - transformOperation.x, transformOperation.y, transformOperation.z); + translateX, translateY, transformOperation.z.resolve(0)); } if (transformOperation.type == TransformOperationType::Skew) { - return Transform::Skew(transformOperation.x, transformOperation.y); + return Transform::Skew(transformOperation.x.resolve(0), transformOperation.y.resolve(0)); } if (transformOperation.type == TransformOperationType::Rotate) { return Transform::Rotate( - transformOperation.x, transformOperation.y, transformOperation.z); + transformOperation.x.resolve(0), transformOperation.y.resolve(0), transformOperation.z.resolve(0)); } // Identity or Arbitrary @@ -170,29 +177,32 @@ Transform Transform::FromTransformOperation( TransformOperation Transform::DefaultTransformOperation( TransformOperationType type) { + auto Zero = ValueUnit{0, UnitType::Point}; + auto One = ValueUnit{1, UnitType::Point}; switch (type) { case TransformOperationType::Arbitrary: - return TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}; + return TransformOperation{TransformOperationType::Arbitrary, Zero, Zero, Zero}; case TransformOperationType::Perspective: - return TransformOperation{TransformOperationType::Perspective, 0, 0, 0}; + return TransformOperation{TransformOperationType::Perspective, Zero, Zero, Zero}; case TransformOperationType::Scale: - return TransformOperation{TransformOperationType::Scale, 1, 1, 1}; + return TransformOperation{TransformOperationType::Scale, One, One, One}; case TransformOperationType::Translate: - return TransformOperation{TransformOperationType::Translate, 0, 0, 0}; + return TransformOperation{TransformOperationType::Translate, Zero, Zero, Zero}; case TransformOperationType::Rotate: - return TransformOperation{TransformOperationType::Rotate, 0, 0, 0}; + return TransformOperation{TransformOperationType::Rotate, Zero, Zero, Zero}; case TransformOperationType::Skew: - return TransformOperation{TransformOperationType::Skew, 0, 0, 0}; + return TransformOperation{TransformOperationType::Skew, Zero, Zero, Zero}; default: case TransformOperationType::Identity: - return TransformOperation{TransformOperationType::Identity, 0, 0, 0}; + return TransformOperation{TransformOperationType::Identity, Zero, Zero, Zero}; } } Transform Transform::Interpolate( Float animationProgress, const Transform& lhs, - const Transform& rhs) { + const Transform& rhs, + const Size& size) { // Iterate through operations and reconstruct an interpolated resulting // transform If at any point we hit an "Arbitrary" Transform, return at that // point @@ -240,9 +250,12 @@ Transform Transform::Interpolate( result = result * Transform::FromTransformOperation(TransformOperation{ type, - lhsOp.x + (rhsOp.x - lhsOp.x) * animationProgress, - lhsOp.y + (rhsOp.y - lhsOp.y) * animationProgress, - lhsOp.z + (rhsOp.z - lhsOp.z) * animationProgress}); + ValueUnit(lhsOp.x.resolve(size.width) + (rhsOp.x.resolve(size.width) - lhsOp.x.resolve(size.width)) + * animationProgress, UnitType::Point), + ValueUnit(lhsOp.y.resolve(size.height) + (rhsOp.y.resolve(size.height) - lhsOp.y.resolve(size.height)) + * animationProgress, UnitType::Point), + ValueUnit(lhsOp.z.resolve(0) + (rhsOp.z.resolve(0) - lhsOp.z.resolve(0)) + * animationProgress, UnitType::Point)}, size); } return result; @@ -262,6 +275,14 @@ bool Transform::operator==(const Transform& rhs) const { return false; } } + if (this->operations.size() != rhs.operations.size()) { + return false; + } + for (size_t i = 0; i < this->operations.size(); i++) { + if (this->operations[i] != rhs.operations[i]) { + return false; + } + } return true; } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h index 1ed8353862b8ee..64d8bfe61a7564 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h @@ -17,6 +17,7 @@ #include #include #include +#include #ifdef ANDROID #include @@ -47,9 +48,10 @@ enum class TransformOperationType { struct TransformOperation { TransformOperationType type; - Float x; - Float y; - Float z; + ValueUnit x; + ValueUnit y; + ValueUnit z; + bool operator==(const TransformOperation& other) const = default; }; struct TransformOrigin { @@ -88,7 +90,7 @@ struct Transform { * Given a TransformOperation, return the proper transform. */ static Transform FromTransformOperation( - TransformOperation transformOperation); + TransformOperation transformOperation, const Size& size); static TransformOperation DefaultTransformOperation( TransformOperationType type); @@ -151,7 +153,8 @@ struct Transform { static Transform Interpolate( Float animationProgress, const Transform& lhs, - const Transform& rhs); + const Transform& rhs, + const Size& size); static bool isVerticalInversion(const Transform& transform); static bool isHorizontalInversion(const Transform& transform); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp new file mode 100644 index 00000000000000..0acef0dd0330c6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.cpp @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include "ValueUnit.h" + +#include + +namespace facebook::react { + +ValueUnit ValueUnit::getValueUnitFromRawValue(const RawValue& value) { + if (value.hasType()) { + auto number = (float)value; + return ValueUnit(number, UnitType::Point); + } else if (value.hasType()) { + const auto stringValue = (std::string)value; + if (stringValue.back() == '%') { + auto tryValue = folly::tryTo( + std::string_view(stringValue).substr(0, stringValue.length() - 1)); + if (tryValue.hasValue()) { + return ValueUnit(tryValue.value(), UnitType::Percent); + } + } + } + return ValueUnit(); +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h index 6d0d535c3b88e8..bc75f42d167f4c 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h @@ -6,6 +6,7 @@ */ #pragma once +#include namespace facebook::react { @@ -28,5 +29,18 @@ struct ValueUnit { bool operator!=(const ValueUnit& other) const { return !(*this == other); } + + constexpr float resolve(float referenceLength) { + switch (unit) { + case UnitType::Point: + return value; + case UnitType::Percent: + return value * referenceLength * 0.01f; + default: + return 0.0f; + } + } + + static ValueUnit getValueUnitFromRawValue(const RawValue& value); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp index dcbb3052e68b10..94f1d331132d80 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp @@ -28,6 +28,23 @@ TEST(TransformTest, transformingPoint) { EXPECT_EQ(translatedPoint.y, 100); } +TEST(TransformTest, fromTransformOperationPercentage) { + auto point = facebook::react::Point{0, 0}; + Size size = {120, 200}; + + auto operation = TransformOperation{ + TransformOperationType::Translate, ValueUnit{50.0f, UnitType::Percent}, ValueUnit{20.0f, UnitType::Percent}}; + auto translatedPoint = point * Transform::FromTransformOperation(operation, size); + EXPECT_EQ(translatedPoint.x, 60); + EXPECT_EQ(translatedPoint.y, 40); + + operation = TransformOperation{ + TransformOperationType::Translate, ValueUnit{40.0f, UnitType::Percent}, ValueUnit{20.0f, UnitType::Point}}; + translatedPoint = point * Transform::FromTransformOperation(operation, size); + EXPECT_EQ(translatedPoint.x, 48); + EXPECT_EQ(translatedPoint.y, 20); +} + TEST(TransformTest, scalingRect) { auto point = facebook::react::Point{100, 200}; auto size = facebook::react::Size{300, 400}; diff --git a/packages/rn-tester/js/examples/Transform/TransformExample.js b/packages/rn-tester/js/examples/Transform/TransformExample.js index 75370c931cc47c..b163d8ba1587ce 100644 --- a/packages/rn-tester/js/examples/Transform/TransformExample.js +++ b/packages/rn-tester/js/examples/Transform/TransformExample.js @@ -139,6 +139,10 @@ function Flip() { ); } +function TranslatePercentage() { + return ; +} + const styles = StyleSheet.create({ container: { height: 500, @@ -277,6 +281,12 @@ const styles = StyleSheet.create({ height: 100, transformOrigin: 'top left', }, + translatePercentageView: { + transform: 'translate(50%)', + padding: 50, + alignSelf: 'flex-start', + backgroundColor: 'lightblue', + }, }); exports.title = 'Transforms'; @@ -396,4 +406,11 @@ exports.examples = [ return ; }, }, + { + title: 'Translate Percentage', + description: "transform: 'translate(50%)'", + render(): Node { + return ; + }, + }, ];