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: https://github.com/facebook/react-native/pull/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 - https://github.com/facebook/react-native/pull/43193, https://github.com/facebook/react-native/pull/43191

Reviewed By: javache

Differential Revision: D56802425

Pulled By: NickGerleman

fbshipit-source-id: 978cbbdde004afe1e68ffee9a3c7eb7d16336b46
This commit is contained in:
Nishan
2024-05-16 19:35:15 -07:00
committed by Facebook GitHub Bot
parent 82c6f8a580
commit f997b81288
14 changed files with 347 additions and 146 deletions
@@ -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}}"`;
@@ -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}]),
@@ -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<string | number> | 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':
@@ -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);
}
@@ -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;
@@ -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
@@ -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<float, 3> 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<float, 3> 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 {
@@ -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<const ViewProps*>(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
@@ -480,88 +480,6 @@ inline Float toRadians(
return static_cast<Float>(num); // assume suffix is "rad"
}
inline void fromRawValue(
const PropsParserContext& context,
const RawValue& value,
Transform& result) {
auto transformMatrix = Transform{};
react_native_expect(value.hasType<std::vector<RawValue>>());
if (!value.hasType<std::vector<RawValue>>()) {
result = transformMatrix;
return;
}
auto configurations = static_cast<std::vector<RawValue>>(value);
for (const auto& configuration : configurations) {
if (!configuration.hasType<std::unordered_map<std::string, RawValue>>()) {
// TODO: The following checks have to be removed after codegen is shipped.
// See T45151459.
continue;
}
auto configurationPair =
static_cast<std::unordered_map<std::string, RawValue>>(configuration);
auto pair = configurationPair.begin();
auto operation = pair->first;
auto& parameters = pair->second;
if (operation == "matrix") {
react_native_expect(parameters.hasType<std::vector<Float>>());
auto numbers = (std::vector<Float>)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<Float>)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<std::vector<RawValue>>());
if (!value.hasType<std::vector<RawValue>>()) {
result = transformMatrix;
return;
}
auto configurations = static_cast<std::vector<RawValue>>(value);
for (const auto& configuration : configurations) {
if (!configuration.hasType<std::unordered_map<std::string, RawValue>>()) {
// TODO: The following checks have to be removed after codegen is shipped.
// See T45151459.
continue;
}
auto configurationPair =
static_cast<std::unordered_map<std::string, RawValue>>(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<std::vector<Float>>());
auto numbers = (std::vector<Float>)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<RawValue>)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,
@@ -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;
}
@@ -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);
@@ -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
@@ -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);
@@ -139,6 +139,10 @@ function Flip() {
);
}
function TranslatePercentage() {
return <View style={styles.translatePercentageView} />;
}
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 <TransformOriginExample />;
},
},
{
title: 'Translate Percentage',
description: "transform: 'translate(50%)'",
render(): Node {
return <TranslatePercentage />;
},
},
];