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): percentage support in translate #43192

Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}]),
Expand Down
16 changes: 14 additions & 2 deletions packages/react-native/Libraries/StyleSheet/processTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const _getKeyAndValueFromCSSTransform: (
| $TEMPORARY$string<'translateY'>,
args: string,
) => {key: string, value?: number[] | number | string} = (key, args) => {
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z]+)?/g);
const argsWithUnitsRegex = new RegExp(/([+-]?\d+(\.\d+)?)([a-zA-Z%]+)?/g);

switch (key) {
case 'matrix':
Expand All @@ -88,7 +88,11 @@ const _getKeyAndValueFromCSSTransform: (
missingUnitOfMeasurement = true;
}

parsedArgs.push(value);
if (unitOfMeasurement === '%') {
parsedArgs.push(`${value}%`);
} else {
parsedArgs.push(value);
}
}

if (__DEV__) {
Expand Down Expand Up @@ -256,6 +260,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),
);
Comment on lines +266 to +272
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is an existing pattern, but for the future, we've been trying to move towards no-oping on invalid values, instead of creating a redbox, or something more disruptive.

Even longer into the future (not worth thinking about yet), on Fabric side, we want to just do all this parsing in native.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this parsing does happen in native. validateTransform is just an additional DEV-time only validation of the input to get better signal to the developer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, but we have though redbox during development on invalid style is too aggressive/disruptive.

break;
case 'scale':
case 'scaleX':
case 'scaleY':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}

if (_props->transformOrigin.isSet()) {
if (_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) {
auto newTransform = _props->resolveTransform(layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,10 @@ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView(
propsParserContext,
progress,
startingView.props,
finalView.props);
finalView.props,
finalView.layoutMetrics.frame.size.width,
finalView.layoutMetrics.frame.size.height
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double check that the spec defaults to border-box for reference box by default, so this should be good for now.

);

react_native_assert(mutatedShadowView.props != nullptr);
if (mutatedShadowView.props == nullptr) {
Expand Down Expand Up @@ -1627,7 +1630,9 @@ Props::Shared LayoutAnimationKeyFrameManager::interpolateProps(
const PropsParserContext& context,
Float animationProgress,
const Props::Shared& props,
const Props::Shared& newProps) const {
const Props::Shared& newProps,
Float viewWidth,
Float viewHeight) const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use Size? https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/renderer/graphics/Size.h

Suggested change
Float viewWidth,
Float viewHeight) const {
const Size& size) const {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

#ifdef ANDROID
// On Android only, the merged props should have the same RawProps as the
// final props struct
Expand All @@ -1640,11 +1645,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, viewWidth, viewHeight);
}

return interpolatedPropsShared;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate,
const PropsParserContext& context,
Float animationProgress,
const Props::Shared& props,
const Props::Shared& newProps) const;
const Props::Shared& newProps,
Float viewWidth,
Float viewHeight) const;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -392,19 +392,33 @@ Transform BaseViewProps::resolveTransform(
LayoutMetrics const& 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different from previous behaviour which returned the plain transform. Not sure it matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Added this as an optimisation to avoid the operations loop as FromTransformOperation has become size dependent (we can also prevent calling this method when size is 0). I am also not sure transformation with zero dimensions matter (with border-box). It's an assumption though.

At some point I would like to explore moving transform matrix generation to shadow view and native view can implement a method, similar to how updateLayoutMetrics works.

}
std::array<float, 3> 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 = transformMatrix * transform;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid the multiplication

Suggested change
transformMatrix = transformMatrix * transform;
transformMatrix = transform;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

} else {
for (size_t i = 0; i < transform.operations.size(); i++) {
auto transformFromOperation = transform.FromTransformOperation(transform.operations[i], viewWidth, viewHeight);
transformMatrix = transformMatrix * transformFromOperation;
}
}

if (transformOrigin.isSet()) {
std::array<float, 3> 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]);
}
Comment on lines +420 to +428
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do this before the transform processing loop? That way we can avoid less intermediate objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do translateOffsets[0], translateOffsets[1], translateOffsets[2]) before the loop but -translateOffsets[0], -translateOffsets[1], -translateOffsets[2] needs to be done post processing for transform origin to work correctly. I am missing how it can lead to less intermediate objects. Let me know, happy to fix it! Also, we want to prevent additional transformOrigin.isSet() checks.


return transformMatrix;
}

bool BaseViewProps::getClipsContentToBounds() const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ static inline void interpolateViewProps(
Float animationProgress,
const Props::Shared& oldPropsShared,
const Props::Shared& newPropsShared,
Props::Shared& interpolatedPropsShared) {
Props::Shared& interpolatedPropsShared,
Float viewWidth,
Float viewHeight) {
const ViewProps* oldViewProps =
static_cast<const ViewProps*>(oldPropsShared.get());
const ViewProps* newViewProps =
Expand All @@ -31,9 +33,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, viewWidth, viewHeight);

// Android uses RawProps, not props, to update props on the platform...
// Since interpolated props don't interpolate at all using RawProps, we need
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,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<std::vector<Float>>());
Expand All @@ -512,48 +514,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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed on my initial look-through that we are using ValueUnit (Point, or Percentage) for operations which take radians, which is a little confusing.

Don't want to make this PR too big, but we would replace ValueUnit will CSSValueVariant after the dust on that settles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radians

You mean operations like this right? transformMatrix.operations.push_back( TransformOperation{TransformOperationType::Rotate, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero, Zero})

Yeah, I considered UnitType::Point like a regular Float as ValueUnit eventually will get replaced and to keep the PR small but we can add Angle units in CSSValueVariant.

Replacing ValueUnit with CSSValueVariant

For sure, i can see it is in renderer/css, can we import and start using it? We can also replace ValueUnit in transformOrigin, i guess a separate PR to do that would be better.

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<Float>)parameters;
transformMatrix = transformMatrix *
Transform::Translate(numbers.at(0), numbers.at(1), 0);
auto numbers = (std::vector<RawValue>)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});
}
}

Expand Down