From 25478321f9d88730d006221a90f5d84f32d3ffcd Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 3 Oct 2023 13:58:03 -0700 Subject: [PATCH] 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 --- .../react/uimanager/ReactShadowNode.java | 4 +- .../react/uimanager/ReactShadowNodeImpl.java | 31 ++++++++-- .../react/uimanager/UIImplementation.java | 56 +++++++++++-------- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java index db10bf379e46ba..efaba33cdc3f3a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -123,9 +123,9 @@ public interface 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, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java index f62e6085ae459f..1bb6e9ee4c1b6a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java @@ -337,9 +337,32 @@ public void onAfterUpdateTransaction() { @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 boolean dispatchUpdates( getScreenHeight()); } } - - return layoutHasChanged; - } else { - return false; } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index f15004e38e91d2..894f32380bda2c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -29,7 +29,9 @@ 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 @@ protected void updateViewHierarchy() { .arg("rootTag", cssRoot.getReactTag()) .flush(); try { - applyUpdatesRecursive(cssRoot, 0f, 0f); + List 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 @@ protected void calculateRootLayout(ReactShadowNode cssRoot) { } } - protected void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) { + protected void applyUpdatesRecursive( + ReactShadowNode cssNode, + float absoluteX, + float absoluteY, + List onLayoutNodes) { if (!cssNode.hasUpdates()) { return; } + if (cssNode.dispatchUpdatesWillChangeLayout(absoluteX, absoluteY) + && cssNode.shouldNotifyOnLayout() + && !mShadowNodeRegistry.isRootNode(cssNode.getReactTag())) { + onLayoutNodes.add(cssNode); + } + Iterable 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); - - // 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.dispatchUpdates(absoluteX, absoluteY, mOperationsQueue, mNativeViewHierarchyOptimizer); + cssNode.markUpdateSeen(); mNativeViewHierarchyOptimizer.onViewUpdatesCompleted(cssNode); }