Top-down onLayout events (#39644)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/39644

This makes Android Paper/Classic renderer fire `onLayout` events top down, like in Fabric/new Architecture. This gives a much more sane model for using layout events to calculate bottom/right-edge insets.

I was under the impression that Paper in general was bottom-up, but it turns out that is only true for Android and Windows (iOS seems totally deterministic).

This is a behavior change, but to my knowledge was never hit during the Fabric migration, and any JS code already written for both Android and iOS cannot make assumptions here anyways.

Changelog:
[General][Changed] - Make layout events top-down on Android classic renderer

Reviewed By: mdvacca

Differential Revision: D49627996

fbshipit-source-id: 29964b421dd420681d45348c7db16f211a6c087f
This commit is contained in:
Nick Gerleman
2023-10-03 13:58:03 -07:00
committed by Facebook GitHub Bot
parent ea88338d75
commit 25478321f9
3 changed files with 59 additions and 30 deletions
@@ -123,9 +123,9 @@ public interface ReactShadowNode<T extends ReactShadowNode> {
*/
void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue);
/** @return true if layout (position or dimensions) changed, false otherwise. */
/* package */ boolean dispatchUpdatesWillChangeLayout(float absoluteX, float absoluteY);
/* package */ boolean dispatchUpdates(
/* package */ void dispatchUpdates(
float absoluteX,
float absoluteY,
UIViewOperationQueue uiViewOperationQueue,
@@ -337,9 +337,32 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
@Override
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {}
/** @return true if layout (position or dimensions) changed, false otherwise. */
@Override
public boolean dispatchUpdates(
public boolean dispatchUpdatesWillChangeLayout(float absoluteX, float absoluteY) {
if (!hasNewLayout()) {
return false;
}
float layoutX = getLayoutX();
float layoutY = getLayoutY();
int newAbsoluteLeft = Math.round(absoluteX + layoutX);
int newAbsoluteTop = Math.round(absoluteY + layoutY);
int newAbsoluteRight = Math.round(absoluteX + layoutX + getLayoutWidth());
int newAbsoluteBottom = Math.round(absoluteY + layoutY + getLayoutHeight());
int newScreenX = Math.round(layoutX);
int newScreenY = Math.round(layoutY);
int newScreenWidth = newAbsoluteRight - newAbsoluteLeft;
int newScreenHeight = newAbsoluteBottom - newAbsoluteTop;
return newScreenX != mScreenX
|| newScreenY != mScreenY
|| newScreenWidth != mScreenWidth
|| newScreenHeight != mScreenHeight;
}
@Override
public void dispatchUpdates(
float absoluteX,
float absoluteY,
UIViewOperationQueue uiViewOperationQueue,
@@ -386,10 +409,6 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
getScreenHeight());
}
}
return layoutHasChanged;
} else {
return false;
}
}
@@ -29,7 +29,9 @@ import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
@@ -665,7 +667,20 @@ public class UIImplementation {
.arg("rootTag", cssRoot.getReactTag())
.flush();
try {
applyUpdatesRecursive(cssRoot, 0f, 0f);
List<ReactShadowNode> onLayoutNodes = new ArrayList<>();
applyUpdatesRecursive(cssRoot, 0f, 0f, onLayoutNodes);
for (ReactShadowNode node : onLayoutNodes) {
mEventDispatcher.dispatchEvent(
OnLayoutEvent.obtain(
-1, /* surfaceId not used in classic renderer */
node.getReactTag(),
node.getScreenX(),
node.getScreenY(),
node.getScreenWidth(),
node.getScreenHeight()));
}
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
@@ -951,39 +966,34 @@ public class UIImplementation {
}
}
protected void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) {
protected void applyUpdatesRecursive(
ReactShadowNode cssNode,
float absoluteX,
float absoluteY,
List<ReactShadowNode> onLayoutNodes) {
if (!cssNode.hasUpdates()) {
return;
}
if (cssNode.dispatchUpdatesWillChangeLayout(absoluteX, absoluteY)
&& cssNode.shouldNotifyOnLayout()
&& !mShadowNodeRegistry.isRootNode(cssNode.getReactTag())) {
onLayoutNodes.add(cssNode);
}
Iterable<? extends ReactShadowNode> cssChildren = cssNode.calculateLayoutOnChildren();
if (cssChildren != null) {
for (ReactShadowNode cssChild : cssChildren) {
applyUpdatesRecursive(
cssChild, absoluteX + cssNode.getLayoutX(), absoluteY + cssNode.getLayoutY());
cssChild,
absoluteX + cssNode.getLayoutX(),
absoluteY + cssNode.getLayoutY(),
onLayoutNodes);
}
}
int tag = cssNode.getReactTag();
if (!mShadowNodeRegistry.isRootNode(tag)) {
boolean frameDidChange =
cssNode.dispatchUpdates(
absoluteX, absoluteY, mOperationsQueue, mNativeViewHierarchyOptimizer);
cssNode.dispatchUpdates(absoluteX, absoluteY, mOperationsQueue, mNativeViewHierarchyOptimizer);
// Notify JS about layout event if requested
// and if the position or dimensions actually changed
// (consistent with iOS).
if (frameDidChange && cssNode.shouldNotifyOnLayout()) {
mEventDispatcher.dispatchEvent(
OnLayoutEvent.obtain(
-1, /* surfaceId not used in classic renderer */
tag,
cssNode.getScreenX(),
cssNode.getScreenY(),
cssNode.getScreenWidth(),
cssNode.getScreenHeight()));
}
}
cssNode.markUpdateSeen();
mNativeViewHierarchyOptimizer.onViewUpdatesCompleted(cssNode);
}