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 dc8d045189e..119c7344a13 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 66ce708c033..fe7c5d28515 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 a703ac47161..6310a4b5c77 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 2083be4a318..b52a195c2cf 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -421,7 +421,8 @@ using namespace facebook::react; _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 25fe0fd632e..5684bf95bc0 100644 --- a/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp @@ -1112,12 +1112,14 @@ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView( // Animate opacity or scale/transform PropsParserContext propsParserContext{ finalView.surfaceId, *contextContainer_}; + const auto& finalViewSize = finalView.layoutMetrics.frame.size; mutatedShadowView.props = interpolateProps( componentDescriptor, propsParserContext, progress, startingView.props, - finalView.props); + finalView.props, + finalViewSize); react_native_assert(mutatedShadowView.props != nullptr); if (mutatedShadowView.props == nullptr) { @@ -1626,7 +1628,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 @@ -1643,7 +1646,7 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps( 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 04b5acc45ed..82342767c4c 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 ffd92fbb15b..cb8e771ea99 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -458,21 +458,36 @@ BorderMetrics BaseViewProps::resolveBorderMetrics( 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; + const auto& frameSize = layoutMetrics.frame.size; + auto transformMatrix = Transform{}; + if (frameSize.width == 0 && frameSize.height == 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::Translate( - -translateOffsets[0], -translateOffsets[1], -translateOffsets[2]); - return newTransform; + + // transform is matrix + if (transform.operations.size() == 1 && + transform.operations[0].type == TransformOperationType::Arbitrary) { + transformMatrix = transform; + } else { + for (const auto& operation : transform.operations) { + transformMatrix = transformMatrix * + Transform::FromTransformOperation( + operation, layoutMetrics.frame.size); + } + } + + if (transformOrigin.isSet()) { + std::array translateOffsets = getTranslateForTransformOrigin( + frameSize.width, frameSize.height, transformOrigin); + transformMatrix = + Transform::Translate( + 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 2e95558c493..1aec3ac18ac 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,11 @@ 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 f10bcbb9cbb..460528ef0c8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -480,88 +480,6 @@ inline Float toRadians( return static_cast(num); // assume suffix is "rad" } -inline void fromRawValue( - const PropsParserContext& context, - const RawValue& value, - Transform& result) { - auto transformMatrix = Transform{}; - react_native_expect(value.hasType>()); - if (!value.hasType>()) { - result = transformMatrix; - return; - } - - auto configurations = static_cast>(value); - for (const auto& configuration : configurations) { - if (!configuration.hasType>()) { - // TODO: The following checks have to be removed after codegen is shipped. - // See T45151459. - continue; - } - - auto configurationPair = - static_cast>(configuration); - auto pair = configurationPair.begin(); - auto operation = pair->first; - auto& parameters = pair->second; - - if (operation == "matrix") { - react_native_expect(parameters.hasType>()); - auto numbers = (std::vector)parameters; - react_native_expect(numbers.size() == transformMatrix.matrix.size()); - auto i = 0; - for (auto number : numbers) { - transformMatrix.matrix[i++] = number; - } - transformMatrix.operations.push_back( - TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}); - } else if (operation == "perspective") { - transformMatrix = - transformMatrix * Transform::Perspective((Float)parameters); - } else if (operation == "rotateX") { - transformMatrix = transformMatrix * - Transform::Rotate(toRadians(parameters, 0.0f), 0, 0); - } else if (operation == "rotateY") { - transformMatrix = transformMatrix * - Transform::Rotate(0, toRadians(parameters, 0.0f), 0); - } else if (operation == "rotateZ" || operation == "rotate") { - transformMatrix = transformMatrix * - Transform::Rotate(0, 0, toRadians(parameters, 0.0f)); - } else if (operation == "scale") { - auto number = (Float)parameters; - transformMatrix = - transformMatrix * Transform::Scale(number, number, number); - } else if (operation == "scaleX") { - transformMatrix = - transformMatrix * Transform::Scale((Float)parameters, 1, 1); - } else if (operation == "scaleY") { - transformMatrix = - transformMatrix * Transform::Scale(1, (Float)parameters, 1); - } else if (operation == "scaleZ") { - transformMatrix = - transformMatrix * Transform::Scale(1, 1, (Float)parameters); - } else if (operation == "translate") { - auto numbers = (std::vector)parameters; - transformMatrix = transformMatrix * - Transform::Translate(numbers.at(0), numbers.at(1), 0); - } else if (operation == "translateX") { - transformMatrix = - transformMatrix * Transform::Translate((Float)parameters, 0, 0); - } else if (operation == "translateY") { - transformMatrix = - transformMatrix * Transform::Translate(0, (Float)parameters, 0); - } else if (operation == "skewX") { - transformMatrix = - transformMatrix * Transform::Skew(toRadians(parameters, 0.0f), 0); - } else if (operation == "skewY") { - transformMatrix = - transformMatrix * Transform::Skew(0, toRadians(parameters, 0.0f)); - } - } - - result = transformMatrix; -} - inline void fromRawValue( const PropsParserContext& /*context*/, const RawValue& value, @@ -591,6 +509,125 @@ inline void fromRawValue( result = valueUnit; } +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + Transform& result) { + auto transformMatrix = Transform{}; + react_native_expect(value.hasType>()); + if (!value.hasType>()) { + result = transformMatrix; + return; + } + + auto configurations = static_cast>(value); + for (const auto& configuration : configurations) { + if (!configuration.hasType>()) { + // TODO: The following checks have to be removed after codegen is shipped. + // See T45151459. + continue; + } + + auto configurationPair = + static_cast>(configuration); + 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>()); + auto numbers = (std::vector)parameters; + react_native_expect(numbers.size() == transformMatrix.matrix.size()); + auto i = 0; + for (auto number : numbers) { + transformMatrix.matrix[i++] = number; + } + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Arbitrary, Zero, Zero, Zero}); + } else if (operation == "perspective") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Perspective, + ValueUnit((Float)parameters, UnitType::Point), + Zero, + Zero}); + } else if (operation == "rotateX") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Rotate, + ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), + Zero, + Zero}); + } else if (operation == "rotateY") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Rotate, + Zero, + ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), + Zero}); + } else if (operation == "rotateZ" || operation == "rotate") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Rotate, + Zero, + Zero, + ValueUnit(toRadians(parameters, 0.0f), UnitType::Point)}); + } else if (operation == "scale") { + auto number = ValueUnit((Float)parameters, UnitType::Point); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, number, number, number}); + } else if (operation == "scaleX") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, + ValueUnit((Float)parameters, UnitType::Point), + One, + One}); + } else if (operation == "scaleY") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, + One, + ValueUnit((Float)parameters, UnitType::Point), + One}); + } else if (operation == "scaleZ") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Scale, + One, + One, + ValueUnit((Float)parameters, UnitType::Point)}); + } else if (operation == "translate") { + auto numbers = (std::vector)parameters; + ValueUnit valueX; + fromRawValue(context, numbers.at(0), valueX); + ValueUnit valueY; + fromRawValue(context, numbers.at(1), valueY); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, valueX, valueY, Zero}); + } else if (operation == "translateX") { + ValueUnit valueX; + fromRawValue(context, parameters, valueX); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, valueX, Zero, Zero}); + } else if (operation == "translateY") { + ValueUnit valueY; + fromRawValue(context, parameters, valueY); + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Translate, Zero, valueY, Zero}); + } else if (operation == "skewX") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Skew, + ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), + Zero, + Zero}); + } else if (operation == "skewY") { + transformMatrix.operations.push_back(TransformOperation{ + TransformOperationType::Skew, + Zero, + ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), + Zero}); + } + } + + result = transformMatrix; +} + inline void fromRawValue( const PropsParserContext& context, const RawValue& value, diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp index 3fd74379cda..d65299a25d3 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp @@ -41,8 +41,12 @@ 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 +58,10 @@ 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 +76,10 @@ 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; @@ -81,8 +91,11 @@ Transform Transform::Skew(Float x, Float y) { auto transform = Transform{}; Float xprime = isZero(x) ? 0 : x; Float yprime = isZero(y) ? 0 : y; - transform.operations.push_back( - TransformOperation{TransformOperationType::Skew, xprime, yprime, 0}); + transform.operations.push_back(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 +104,12 @@ Transform Transform::Skew(Float x, Float y) { Transform Transform::RotateX(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { - transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, radians, 0, 0}); + auto Zero = ValueUnit(0, UnitType::Point); + transform.operations.push_back(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 +121,12 @@ Transform Transform::RotateX(Float radians) { Transform Transform::RotateY(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { - transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, 0, radians, 0}); + auto Zero = ValueUnit(0, UnitType::Point); + transform.operations.push_back(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 +138,12 @@ Transform Transform::RotateY(Float radians) { Transform Transform::RotateZ(Float radians) { auto transform = Transform{}; if (!isZero(radians)) { - transform.operations.push_back( - TransformOperation{TransformOperationType::Rotate, 0, 0, radians}); + auto Zero = ValueUnit(0, UnitType::Point); + transform.operations.push_back(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); @@ -142,24 +167,33 @@ 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 @@ -168,29 +202,37 @@ 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 @@ -236,11 +278,27 @@ Transform Transform::Interpolate( react_native_assert(type == rhsOp.type); 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}); + Transform::FromTransformOperation( + TransformOperation{ + type, + 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; @@ -260,6 +318,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 1ed8353862b..e68b80b9dba 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h @@ -47,9 +47,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 +89,8 @@ 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.h b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h index 6d0d535c3b8..ae2363745de 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h @@ -28,5 +28,16 @@ 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; + } + } }; } // 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 fd3c626bd5f..9a9719e6279 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,30 @@ TEST(TransformTest, transformingPoint) { EXPECT_EQ(translatedPoint.y, 100); } +TEST(TransformTest, fromTransformOperationPercentage) { + auto point = facebook::react::Point{0, 0}; + facebook::react::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}; @@ -85,9 +109,9 @@ TEST(TransformTest, rotate3dOverload) { auto transform = Transform::Rotate(0, 0, M_PI_4); EXPECT_EQ(transform.operations.size(), 1); EXPECT_EQ(transform.operations[0].type, TransformOperationType::Rotate); - EXPECT_EQ(transform.operations[0].x, 0); - EXPECT_EQ(transform.operations[0].y, 0); - ASSERT_NEAR(transform.operations[0].z, M_PI_4, 0.0001); + EXPECT_EQ(transform.operations[0].x.resolve(0), 0); + EXPECT_EQ(transform.operations[0].y.resolve(0), 0); + ASSERT_NEAR(transform.operations[0].z.resolve(0), M_PI_4, 0.0001); auto transformedRect = rect * Transform::Rotate(0, 0, M_PI_4); diff --git a/packages/rn-tester/js/examples/Transform/TransformExample.js b/packages/rn-tester/js/examples/Transform/TransformExample.js index 75370c931cc..b163d8ba158 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 ; + }, + }, ];