Files
react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp
T
Samuel Susla 43b2c6dd85 Fix conditions to fire onLayout event
Summary:
Changelog: [Internal]

# Problem

## Step 1
JS clones a node that has size {100, 100} and changes props that cause the node to increase size to {200, 200}. JS holds pointer to this node.

Now, the size (stored in LayoutableShadowNode.layoutMetrics_) changes after Yoga layout is triggered.

However, the node gets cloned inside State Reconciliation before Yoga layout phase. The JS pointer points to a node with size {100, 100}, not to a node with size {200, 200}.

## Step 2

Again, JS clones node (with old reference, therefore gets old layoutMetrics_ with size {100, 100}) and it changes props that cause the node to decrease its size back to {100, 100}.
We go all the way to Yoga layout and looking for nodes that have been affected by the node. The node, affected by the layout because it went from {200, 200} to {100, 100}, will be evaluated as not affected. This causes onLayout event to not be fired.

# Fix
We can safely remove the frame equality check (please see below). This can be done because we already check for equality before dispatching onLayout. It happens here:

https://www.internalfb.com/intern/diffusion/FBS/browsefile/master/xplat/js/react-native-github/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp?commit=881853eb0c42625fd0812bd2652bf36fcbd614ee&lines=43

As far as I know, `affectedNodes` isn't used for anything else besides dispatching onLayout.

# Discussion

This problem manifests itself only when a node has two different sizes that it flips between. To better understand this, please watch the video in Test plan labelled "before". Notice how the text has 2 different values that it flips between.

Here is a code that was affected by it https://fburl.com/diffusion/3hwo0iy5
If you inspect it closely, you will notice that it depends on `onLayout` to return correct value to calculate offset from left.

Reviewed By: JoshuaGross

Differential Revision: D22999891

fbshipit-source-id: e2d0f5771c1bf3cd788e5e9da0155c92e33fb84e
2020-08-10 05:11:29 -07:00

283 lines
8.7 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "LayoutableShadowNode.h"
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/debug/DebugStringConvertibleItem.h>
#include <react/renderer/graphics/conversions.h>
namespace facebook {
namespace react {
LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics(
ShadowNodeFamily const &descendantNodeFamily,
LayoutableShadowNode const &ancestorNode,
LayoutInspectingPolicy policy) {
if (&descendantNodeFamily == &ancestorNode.getFamily()) {
// Layout metrics of a node computed relatively to the same node are equal
// to `transform`-ed layout metrics of the node with zero `origin`.
auto layoutMetrics = ancestorNode.getLayoutMetrics();
if (policy.includeTransform) {
layoutMetrics.frame = layoutMetrics.frame * ancestorNode.getTransform();
}
layoutMetrics.frame.origin = {0, 0};
return layoutMetrics;
}
auto ancestors = descendantNodeFamily.getAncestors(ancestorNode);
if (ancestors.size() == 0) {
// Specified nodes do not form an ancestor-descender relationship
// in the same tree. Aborting.
return EmptyLayoutMetrics;
}
// Step 1.
// Creating a list of nodes that form a chain from the descender node to
// ancestor node inclusively.
auto shadowNodeList = better::small_vector<ShadowNode const *, 16>{};
// Finding the measured node.
// The last element in the `AncestorList` is a pair of a parent of the node
// and an index of this node in the parent's children list.
auto &pair = ancestors.at(ancestors.size() - 1);
auto descendantNode = pair.first.get().getChildren().at(pair.second).get();
// Putting the node inside the list.
// Even if this is a node with a `RootNodeKind` trait, we don't treat it as
// root because we measure it from an outside tree perspective.
shadowNodeList.push_back(descendantNode);
for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) {
auto &shadowNode = it->first.get();
shadowNodeList.push_back(&shadowNode);
if (shadowNode.getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) {
// If this is a node with a `RootNodeKind` trait, we need to stop right
// there.
break;
}
}
// Step 2.
// Computing the initial size of the measured node.
auto descendantLayoutableNode =
traitCast<LayoutableShadowNode const *>(descendantNode);
if (!descendantLayoutableNode) {
return EmptyLayoutMetrics;
}
auto layoutMetrics = descendantLayoutableNode->getLayoutMetrics();
auto &resultFrame = layoutMetrics.frame;
resultFrame.origin = {0, 0};
// Step 3.
// Iterating on a list of nodes computing compound offset.
auto size = shadowNodeList.size();
for (size_t i = 0; i < size; i++) {
auto currentShadowNode =
traitCast<LayoutableShadowNode const *>(shadowNodeList.at(i));
if (!currentShadowNode) {
return EmptyLayoutMetrics;
}
auto currentFrame = currentShadowNode->getLayoutMetrics().frame;
if (i == size - 1) {
// If it's the last element, its origin is irrelevant.
currentFrame.origin = {0, 0};
}
if (policy.includeTransform) {
resultFrame.size = resultFrame.size * currentShadowNode->getTransform();
currentFrame = currentFrame * currentShadowNode->getTransform();
}
resultFrame.origin += currentFrame.origin;
if (i != 0 && policy.includeTransform) {
resultFrame.origin += currentShadowNode->getContentOriginOffset();
}
}
return layoutMetrics;
}
LayoutableShadowNode::LayoutableShadowNode(
ShadowNodeFragment const &fragment,
ShadowNodeFamily::Shared const &family,
ShadowNodeTraits traits)
: ShadowNode(fragment, family, traits), layoutMetrics_({}) {}
LayoutableShadowNode::LayoutableShadowNode(
ShadowNode const &sourceShadowNode,
ShadowNodeFragment const &fragment)
: ShadowNode(sourceShadowNode, fragment),
layoutMetrics_(static_cast<LayoutableShadowNode const &>(sourceShadowNode)
.layoutMetrics_) {}
ShadowNodeTraits LayoutableShadowNode::BaseTraits() {
auto traits = ShadowNodeTraits{};
traits.set(ShadowNodeTraits::Trait::LayoutableKind);
return traits;
}
LayoutMetrics LayoutableShadowNode::getLayoutMetrics() const {
return layoutMetrics_;
}
void LayoutableShadowNode::setLayoutMetrics(LayoutMetrics layoutMetrics) {
ensureUnsealed();
if (layoutMetrics_ == layoutMetrics) {
return;
}
layoutMetrics_ = layoutMetrics;
}
Transform LayoutableShadowNode::getTransform() const {
return Transform::Identity();
}
Point LayoutableShadowNode::getContentOriginOffset() const {
return {0, 0};
}
LayoutMetrics LayoutableShadowNode::getRelativeLayoutMetrics(
LayoutableShadowNode const &ancestorLayoutableShadowNode,
LayoutInspectingPolicy policy) const {
return computeRelativeLayoutMetrics(
getFamily(), ancestorLayoutableShadowNode, policy);
}
LayoutableShadowNode::UnsharedList
LayoutableShadowNode::getLayoutableChildNodes() const {
LayoutableShadowNode::UnsharedList layoutableChildren;
for (const auto &childShadowNode : getChildren()) {
auto layoutableChildShadowNode =
traitCast<LayoutableShadowNode const *>(childShadowNode.get());
if (layoutableChildShadowNode) {
layoutableChildren.push_back(
const_cast<LayoutableShadowNode *>(layoutableChildShadowNode));
}
}
return layoutableChildren;
}
Size LayoutableShadowNode::measureContent(
LayoutContext const &layoutContext,
LayoutConstraints const &layoutConstraints) const {
return Size();
}
Size LayoutableShadowNode::measure(
LayoutContext const &layoutContext,
LayoutConstraints const &layoutConstraints) const {
auto clonedShadowNode = clone({});
auto &layoutableShadowNode =
static_cast<LayoutableShadowNode &>(*clonedShadowNode);
auto localLayoutContext = layoutContext;
localLayoutContext.affectedNodes = nullptr;
layoutableShadowNode.layoutTree(localLayoutContext, layoutConstraints);
return layoutableShadowNode.getLayoutMetrics().frame.size;
}
Float LayoutableShadowNode::firstBaseline(Size size) const {
return 0;
}
Float LayoutableShadowNode::lastBaseline(Size size) const {
return 0;
}
void LayoutableShadowNode::layoutTree(
LayoutContext layoutContext,
LayoutConstraints layoutConstraints) {
// Default implementation does nothing.
}
void LayoutableShadowNode::layout(LayoutContext layoutContext) {
// Default implementation does nothing.
}
ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint(
ShadowNode::Shared node,
Point point) {
auto layoutableShadowNode =
dynamic_cast<const LayoutableShadowNode *>(node.get());
if (!layoutableShadowNode) {
return nullptr;
}
auto frame = layoutableShadowNode->getLayoutMetrics().frame;
auto transformedFrame = frame * layoutableShadowNode->getTransform();
auto isPointInside = transformedFrame.containsPoint(point);
if (!isPointInside) {
return nullptr;
}
auto newPoint = point - transformedFrame.origin -
layoutableShadowNode->getContentOriginOffset();
for (const auto &childShadowNode : node->getChildren()) {
auto hitView = findNodeAtPoint(childShadowNode, newPoint);
if (hitView) {
return hitView;
}
}
return isPointInside ? node : nullptr;
}
#if RN_DEBUG_STRING_CONVERTIBLE
SharedDebugStringConvertibleList LayoutableShadowNode::getDebugProps() const {
auto list = SharedDebugStringConvertibleList{};
if (!getIsLayoutClean()) {
list.push_back(std::make_shared<DebugStringConvertibleItem>("dirty"));
}
auto layoutMetrics = getLayoutMetrics();
auto defaultLayoutMetrics = LayoutMetrics();
list.push_back(std::make_shared<DebugStringConvertibleItem>(
"frame", toString(layoutMetrics.frame)));
if (layoutMetrics.borderWidth != defaultLayoutMetrics.borderWidth) {
list.push_back(std::make_shared<DebugStringConvertibleItem>(
"borderWidth", toString(layoutMetrics.borderWidth)));
}
if (layoutMetrics.contentInsets != defaultLayoutMetrics.contentInsets) {
list.push_back(std::make_shared<DebugStringConvertibleItem>(
"contentInsets", toString(layoutMetrics.contentInsets)));
}
if (layoutMetrics.displayType == DisplayType::None) {
list.push_back(std::make_shared<DebugStringConvertibleItem>("hidden"));
}
if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
list.push_back(std::make_shared<DebugStringConvertibleItem>("rtl"));
}
return list;
}
#endif
} // namespace react
} // namespace facebook