Files
react-native/ReactCommon/react/renderer/components/view/tests/LayoutTest.cpp
T
Xin Chen 0975e96d53 Fix transform when calculate overflowInset
Summary:
This diff fixes overflowInset calculation when a shadow node has transform matrix from a transfrom prop in JS. Specifically, this fixed the use case when transform directly used on a view component. When using Animated.View, it will create an invisible wrapper which will behave correctly with existing logic. This diff bring both use cases to work properly.

When a shadow node has transform on it, it will affect the overflowInset values for its parent nodes, but won't affect its own or any of its child nodes overflowInset values. This is obvious for translateX/Y case, but not for scale case. Take a look at the following case:

```
     ┌────────────────┐                 ┌────────────────┐                      ┌────────────────┐
     │Original Layout │                 │  Translate AB  │                      │    Scale AB    │
     └────────────────┘                 └────────────────┘                      └────────────────┘
                                                        ─────▶           ◀─────                  ─────▶
┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ┐     ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐      ┌ ─ ─ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐
        │ A        │                      │ A        │                             │ A        │
│       │          │        │     │       │          │          │      ├ ─ ─ ─ ─ ─ ┼ ─ ─┌─────┤─ ─ ─ ─ ─ ┤
 ─ ─ ─ ─│─ ─ ─┌───┐┼ ─ ─ ─ ─              │          │                   ◀─ ─ ─    │    │AB   │  ─ ─ ─▶
│       │     │AB ││        │     │ ┌ ─ ─ ┼ ─ ─ ─ ┬──┴┬ ─ ─ ─ ─ ┤      │           │    │     │          │
        └─────┤   ├┘                      └───────┤AB │                            └────┤     │
│             │┌──┴─────────┤     │ │             │   │         │      │ │              │ ┌───┴──────────┤
              ││ABC         │                     │┌──┴─────────┐                       │ │ABC           │
│             │└──┬─────────┤   │ │ │             ││ABC         │    │ │ │              │ │              │
┌───ABD───────┴─┐ │             │                 │└──┬─────────┘    │   ▼              │ └───┬──────────┘
├─────────────┬─┘ │         │   │ │ ├───ABD───────┴─┐ │         │    │ ├────────────────┴──┐  │          │
 ─ ─ ─ ─ ─ ─ ─└───┘─ ─ ─ ─ ─    ▼   └─────────────┬─┘ │              ▼ │      ABD          │  │
                                  └ ┴ ─ ─ ─ ─ ─ ─ ┴───┴ ─ ─ ─ ─ ┘      ├────────────────┬──┘  │          │
                                                                        ─ ─ ─ ─ ─ ─ ─ ─ ┴─────┴ ─ ─ ─ ─ ─
```

For the translate case, only view A has change on the overflowInset values for `right` and `bottom`. Note that the `left` and `top` are not changed as we union before and after transform is applied.

For the scale case, similar things are happening for view A, and both `left`, `right`, and `bottom` values are increased. However, for View AB or any of its children, they only *appear* to be increased, but that is purely cosmetic as it's caused by transform. The actual values are not changed, which will later be converted during render phase to actual pixels on screen.

In summary, overflowInset is affected from child nodes transform matrix to the current node (bottom up), but not from transform matrix on the current node to child nodes (top down). So the correct way to apply transform is to make it only affect calculating `contentFrame` during layout, which collects child nodes layout information and their transforms. The `contentFrame` is then used to decide the overflowInset values for the parent node. The current transform matrix on parent node is never used as it's not affecting overflowInset for the current node or its child nodes.

This diff reflects the context above with added unit test to cover the scale and translate transform matrix.

Changelog:
[Android/IOS][Fixed] - Fixed how we calculate overflowInset with transform matrix

Reviewed By: sammy-SC

Differential Revision: D34433404

fbshipit-source-id: 0e48e4af4cfd5a6dd32a30e7667686e8ef1a7004
2022-03-01 14:33:04 -08:00

364 lines
19 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
namespace facebook {
namespace react {
// Note: the (x, y) origin is always relative to the parent node. You may use
// P482342650 to re-create this test case in playground.
// *******************************************************┌─ABCD:────┐****
// *******************************************************│ {70,-50} │****
// *******************************************************│ {30,60} │****
// *******************************************************│ │****
// *******************************************************│ │****
// *******************┌─A: {0,0}{50,50}──┐****************│ │****
// *******************│ │****************│ │****
// *******************│ ┌─AB:──────┐ │****************│ │****
// *******************│ │ {10,10}{30,90}****************│ │****
// *******************│ │ ┌─ABC: {10,10}{110,20}──────┤ ├───┐
// *******************│ │ │ │ │ │
// *******************│ │ │ └──────────┘ │
// *******************│ │ └──────┬───┬───────────────────────────────┘
// *******************│ │ │ │********************************
// *******************└───┤ ├───┘********************************
// ***********************│ │************************************
// ***********************│ │************************************
// ┌─ABE: {-60,50}{70,20}─┴───┐ │************************************
// │ │ │************************************
// │ │ │************************************
// │ │ │************************************
// │ │ │************************************
// └──────────────────────┬───┘ │************************************
// ***********************│ │************************************
// ***********************└──────────┘************************************
enum TestCase { AS_IS, CLIPPING, TRANSFORM_SCALE, TRANSFORM_TRANSLATE };
class LayoutTest : public ::testing::Test {
protected:
ComponentBuilder builder_;
std::shared_ptr<RootShadowNode> rootShadowNode_;
std::shared_ptr<ViewShadowNode> viewShadowNodeA_;
std::shared_ptr<ViewShadowNode> viewShadowNodeAB_;
std::shared_ptr<ViewShadowNode> viewShadowNodeABC_;
std::shared_ptr<ViewShadowNode> viewShadowNodeABCD_;
std::shared_ptr<ViewShadowNode> viewShadowNodeABE_;
LayoutTest() : builder_(simpleComponentBuilder()) {}
void initialize(TestCase testCase) {
// clang-format off
auto element =
Element<RootShadowNode>()
.reference(rootShadowNode_)
.tag(1)
.props([] {
auto sharedProps = std::make_shared<RootProps>();
auto &props = *sharedProps;
props.layoutConstraints = LayoutConstraints{{0,0}, {500, 500}};
auto &yogaStyle = props.yogaStyle;
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{200, YGUnitPoint};
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.reference(viewShadowNodeA_)
.tag(2)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
auto &yogaStyle = props.yogaStyle;
yogaStyle.positionType() = YGPositionTypeAbsolute;
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{50, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{50, YGUnitPoint};
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.reference(viewShadowNodeAB_)
.tag(3)
.props([=] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
auto &yogaStyle = props.yogaStyle;
yogaStyle.positionType() = YGPositionTypeAbsolute;
yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint};
yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{90, YGUnitPoint};
if (testCase == TRANSFORM_SCALE) {
props.transform = props.transform * Transform::Scale(2, 2, 1);
}
if (testCase == TRANSFORM_TRANSLATE) {
props.transform = props.transform * Transform::Translate(10, 10, 0);
}
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.reference(viewShadowNodeABC_)
.tag(4)
.props([=] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
auto &yogaStyle = props.yogaStyle;
if (testCase == CLIPPING) {
yogaStyle.overflow() = YGOverflowHidden;
}
yogaStyle.positionType() = YGPositionTypeAbsolute;
yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint};
yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{110, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint};
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.reference(viewShadowNodeABCD_)
.tag(5)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
auto &yogaStyle = props.yogaStyle;
yogaStyle.positionType() = YGPositionTypeAbsolute;
yogaStyle.position()[YGEdgeLeft] = YGValue{70, YGUnitPoint};
yogaStyle.position()[YGEdgeTop] = YGValue{-50, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{60, YGUnitPoint};
return sharedProps;
})
}),
Element<ViewShadowNode>()
.reference(viewShadowNodeABE_)
.tag(6)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
auto &yogaStyle = props.yogaStyle;
yogaStyle.positionType() = YGPositionTypeAbsolute;
yogaStyle.position()[YGEdgeLeft] = YGValue{-60, YGUnitPoint};
yogaStyle.position()[YGEdgeTop] = YGValue{50, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionWidth] = YGValue{70, YGUnitPoint};
yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint};
return sharedProps;
})
})
})
});
// clang-format on
builder_.build(element);
rootShadowNode_->layoutIfNeeded();
}
};
// Test the layout as described above with no extra changes
TEST_F(LayoutTest, overflowInsetTest) {
initialize(AS_IS);
auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsA.frame.size.width, 50);
EXPECT_EQ(layoutMetricsA.frame.size.height, 50);
EXPECT_EQ(layoutMetricsA.overflowInset.left, -50);
EXPECT_EQ(layoutMetricsA.overflowInset.top, -30);
EXPECT_EQ(layoutMetricsA.overflowInset.right, -80);
EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -50);
auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsABC.frame.size.width, 110);
EXPECT_EQ(layoutMetricsABC.frame.size.height, 20);
EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50);
EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0);
}
// Test when box ABC has clipping (aka overflow hidden)
TEST_F(LayoutTest, overflowInsetWithClippingTest) {
initialize(CLIPPING);
auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsA.frame.size.width, 50);
EXPECT_EQ(layoutMetricsA.frame.size.height, 50);
EXPECT_EQ(layoutMetricsA.overflowInset.left, -50);
EXPECT_EQ(layoutMetricsA.overflowInset.top, 0);
EXPECT_EQ(layoutMetricsA.overflowInset.right, -80);
EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -50);
auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsABC.frame.size.width, 110);
EXPECT_EQ(layoutMetricsABC.frame.size.height, 20);
EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.top, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0);
}
// Test when box AB translate (10, 10, 0) in transform. The parent node's
// overflowInset will be affected, but the transformed node and its child nodes
// are not affected. Here is an example:
//
// ┌────────────────┐ ┌────────────────┐
// │Original Layout │ │ Translate AB │
// └────────────────┘ └────────────────┘
// ─────▶
// ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ┐ ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐
// │ A │ │ A │
// │ │ │ │ │ │ │ │
// ─ ─ ─ ─│─ ─ ─┌───┐┼ ─ ─ ─ ─ │ │
// │ │ │AB ││ │ │ ┌ ─ ─ ┼ ─ ─ ─ ┬──┴┬ ─ ─ ─ ─ ┤
// └─────┤ ├┘ └───────┤AB │
// │ │┌──┴─────────┤ │ │ │ │ │
// ││ABC │ │┌──┴─────────┐
// │ │└──┬─────────┤ │ │ │ ││ABC │
// ┌───ABD───────┴─┐ │ │ │└──┬─────────┘
// ├─────────────┬─┘ │ │ │ │ ├───ABD───────┴─┐ │ │
// ─ ─ ─ ─ ─ ─ ─└───┘─ ─ ─ ─ ─ ▼ └─────────────┬─┘ │
// └ ┴ ─ ─ ─ ─ ─ ─ ┴───┴ ─ ─ ─ ─ ┘
TEST_F(LayoutTest, overflowInsetTransformTranslateTest) {
initialize(TRANSFORM_TRANSLATE);
auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsA.frame.size.width, 50);
EXPECT_EQ(layoutMetricsA.frame.size.height, 50);
// Change on parent node
// The top/left values are NOT changing as overflowInset is union of before
// and after transform layout. In this case, we move to the right and bottom,
// so the left and top is not changing, while right and bottom values are
// increased.
EXPECT_EQ(layoutMetricsA.overflowInset.left, -50);
EXPECT_EQ(layoutMetricsA.overflowInset.top, -30);
EXPECT_EQ(layoutMetricsA.overflowInset.right, -90);
EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -60);
auto layoutMetricsAB = viewShadowNodeAB_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsAB.frame.size.width, 30);
EXPECT_EQ(layoutMetricsAB.frame.size.height, 90);
// No change on self node with translate transform
EXPECT_EQ(layoutMetricsAB.overflowInset.left, -60);
EXPECT_EQ(layoutMetricsAB.overflowInset.top, -40);
EXPECT_EQ(layoutMetricsAB.overflowInset.right, -90);
EXPECT_EQ(layoutMetricsAB.overflowInset.bottom, 0);
auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsABC.frame.size.width, 110);
EXPECT_EQ(layoutMetricsABC.frame.size.height, 20);
// No change on child node
EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50);
EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0);
}
// Test when box AB scaled 2X in transform. The parent node's overflowInset will
// be affected. However, the transformed node and its child nodes only appears
// to be affected (dashed arrow). Since all transform is cosmetic only, the
// actual values are NOT changed. It will be converted later when mapping the
// values to pixels during rendering. Here is an example:
//
// ┌────────────────┐ ┌────────────────┐
// │Original Layout │ │ Scale AB │
// └────────────────┘ └────────────────┘
// ─────▶
// ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐
// │ A │ │ A │
// │ │ │ │ ├ ─ ─ ─ ─ ─ ┼ ─ ─┌─────┤─ ─ ─ ─ ─ ┤
// ─ ─ ─ ─│─ ─ ─┌───┐┼ ─ ─ ─ ─ │ │AB │ ─ ─ ─▶
// │ │ │AB ││ │ │ │ │ │ │
// └─────┤ ├┘ └────┤ │
// │ │┌──┴─────────┤ │ │ ┌───┴──────────┤
// ││ABC │ │ │ABC │
// │ │└──┬─────────┤ │ │ │ │ │
// ┌───ABD───────┴─┐ │ │ │ └───┬──────────┘
// ├─────────────┬─┘ │ │ │ ├────────────────┴──┐ │ │
// ─ ─ ─ ─ ─ ─ ─└───┘─ ─ ─ ─ ─ ▼ │ ABD │ │
// ├────────────────┬──┘ │ │
// ─ ─ ─ ─ ─ ─ ─ ─ ┴─────┴ ─ ─ ─ ─ ─
TEST_F(LayoutTest, overflowInsetTransformScaleTest) {
initialize(TRANSFORM_SCALE);
auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics();
EXPECT_EQ(layoutMetricsA.frame.size.width, 50);
EXPECT_EQ(layoutMetricsA.frame.size.height, 50);
// Change on parent node when a child view scale up
// Note that AB scale up from its center point. The numbers are calculated
// assuming AB's center point is not moving.
EXPECT_EQ(layoutMetricsA.overflowInset.left, -125);
EXPECT_EQ(layoutMetricsA.overflowInset.top, -115);
EXPECT_EQ(layoutMetricsA.overflowInset.right, -185);
EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -95);
auto layoutMetricsAB = viewShadowNodeAB_->getLayoutMetrics();
// The frame of box AB won't actually scale up. The transform matrix is
// purely cosmetic and should apply later in mounting phase.
EXPECT_EQ(layoutMetricsAB.frame.size.width, 30);
EXPECT_EQ(layoutMetricsAB.frame.size.height, 90);
// No change on self node with scale transform. This may sound a bit
// surprising, but the overflowInset values will be scaled up via pixel
// density ratio along with width/height of the view. When we do hit-testing,
// the overflowInset value will appears to be doubled as expected.
EXPECT_EQ(layoutMetricsAB.overflowInset.left, -60);
EXPECT_EQ(layoutMetricsAB.overflowInset.top, -40);
EXPECT_EQ(layoutMetricsAB.overflowInset.right, -90);
EXPECT_EQ(layoutMetricsAB.overflowInset.bottom, 0);
auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics();
// The frame of box ABC won't actually scale up. The transform matrix is
// purely cosmatic and should apply later in mounting phase.
EXPECT_EQ(layoutMetricsABC.frame.size.width, 110);
EXPECT_EQ(layoutMetricsABC.frame.size.height, 20);
// The overflowInset of ABC won't change either. This may sound a bit
// surprising, but the overflowInset values will be scaled up via pixel
// density ratio along with width/height of the view. When we do hit-testing,
// the overflowInset value will appears to be doubled as expected.
EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50);
EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0);
EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0);
}
} // namespace react
} // namespace facebook