mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
82c6f8a580
commit
f997b81288
+1
-1
@@ -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':
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
+6
-3
@@ -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;
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+29
-14
@@ -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 {
|
||||
|
||||
+6
-3
@@ -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 />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user