From 4ca617211b23754ffecf23c1ddc8772bc32420f0 Mon Sep 17 00:00:00 2001 From: David Vacca Date: Fri, 8 Sep 2017 21:07:13 -0700 Subject: [PATCH] Add support for dynamically sized ReactRootView Reviewed By: achen1, AaaChiuuu Differential Revision: D5745093 fbshipit-source-id: 65d85252ab8a0ca38322f49a3d4812380d5228c4 --- .../com/facebook/react/ReactRootView.java | 107 ++++++++++++++- .../react/uimanager/MeasureSpecProvider.java | 17 +++ .../uimanager/NativeViewHierarchyManager.java | 26 ++-- .../react/uimanager/UIImplementation.java | 126 +++++++++++++----- .../react/uimanager/UIManagerModule.java | 33 ++--- 5 files changed, 245 insertions(+), 64 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecProvider.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 7e35a9ef0..5758abdc2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -27,6 +27,7 @@ import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.GuardedRunnable; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarkerConstants; @@ -40,6 +41,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.JSTouchDispatcher; +import com.facebook.react.uimanager.MeasureSpecProvider; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.RootView; import com.facebook.react.uimanager.SizeMonitoringFrameLayout; @@ -60,7 +62,8 @@ import javax.annotation.Nullable; * subsequent touch events related to that gesture (in case when JS code want to handle that * gesture). */ -public class ReactRootView extends SizeMonitoringFrameLayout implements RootView { +public class ReactRootView extends SizeMonitoringFrameLayout + implements RootView, MeasureSpecProvider { /** * Listener interface for react root view events @@ -81,6 +84,9 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView private boolean mIsAttachedToInstance; private boolean mShouldLogContentAppeared; private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); + private boolean mWasMeasured = false; + private int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + private int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); public ReactRootView(Context context) { super(context); @@ -98,19 +104,72 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "ReactRootView.onMeasure"); try { - setMeasuredDimension( - MeasureSpec.getSize(widthMeasureSpec), - MeasureSpec.getSize(heightMeasureSpec)); + mWidthMeasureSpec = widthMeasureSpec; + mHeightMeasureSpec = heightMeasureSpec; + + int width = 0; + int height = 0; + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + int childSize = + child.getLeft() + + child.getMeasuredWidth() + + child.getPaddingLeft() + + child.getPaddingRight(); + width = Math.max(width, childSize); + } + } else { + width = MeasureSpec.getSize(widthMeasureSpec); + } + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + int childSize = + child.getTop() + + child.getMeasuredHeight() + + child.getPaddingTop() + + child.getPaddingBottom(); + height = Math.max(height, childSize); + } + } else { + height = MeasureSpec.getSize(heightMeasureSpec); + } + setMeasuredDimension(width, height); + mWasMeasured = true; // Check if we were waiting for onMeasure to attach the root view. if (mReactInstanceManager != null && !mIsAttachedToInstance) { attachToReactInstanceManager(); + } else { + updateRootLayoutSpecs(mWidthMeasureSpec, mHeightMeasureSpec); } + + enableLayoutCalculation(); + } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } + @Override + public int getWidthMeasureSpec() { + if (!mWasMeasured && getLayoutParams() != null && getLayoutParams().width > 0) { + return MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY); + } + return mWidthMeasureSpec; + } + + @Override + public int getHeightMeasureSpec() { + if (!mWasMeasured && getLayoutParams() != null && getLayoutParams().height > 0) { + return MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY); + } + return mHeightMeasureSpec; + } + @Override public void onChildStartedNativeGesture(MotionEvent androidEvent) { if (mReactInstanceManager == null || !mIsAttachedToInstance || @@ -239,11 +298,51 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView } attachToReactInstanceManager(); + } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } + private void enableLayoutCalculation() { + if (mReactInstanceManager == null) { + FLog.w( + ReactConstants.TAG, + "Unable to enable layout calculation for uninitialized ReactInstanceManager"); + return; + } + final ReactContext reactApplicationContext = mReactInstanceManager.getCurrentReactContext(); + if (reactApplicationContext != null) { + reactApplicationContext + .getCatalystInstance() + .getNativeModule(UIManagerModule.class) + .getUIImplementation() + .enableLayoutCalculationForRootNode(getRootViewTag()); + } + } + + private void updateRootLayoutSpecs(final int widthMeasureSpec, final int heightMeasureSpec) { + if (mReactInstanceManager == null) { + FLog.w( + ReactConstants.TAG, + "Unable to update root layout specs for uninitialized ReactInstanceManager"); + return; + } + final ReactContext reactApplicationContext = mReactInstanceManager.getCurrentReactContext(); + if (reactApplicationContext != null) { + reactApplicationContext.runUIBackgroundRunnable( + new GuardedRunnable(reactApplicationContext) { + @Override + public void runGuarded() { + reactApplicationContext + .getCatalystInstance() + .getNativeModule(UIManagerModule.class) + .updateRootLayoutSpecs(getRootViewTag(), widthMeasureSpec, heightMeasureSpec); + } + }); + } + } + /** * Unmount the react application at this root view, reclaiming any JS memory associated with that * application. If {@link #startReactApplication} is called, this method must be called before the diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecProvider.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecProvider.java new file mode 100644 index 000000000..b03a8362c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecProvider.java @@ -0,0 +1,17 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager; + +import android.view.View; + +/** + * Interface for a {@link View} subclass that provides the width and height measure specs from its + * measure pass. This is currently used to re-measure the root view by reusing the specs for yoga + * layout calculations. + */ +public interface MeasureSpecProvider { + + int getWidthMeasureSpec(); + + int getHeightMeasureSpec(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 23dc3ed7a..e6dda5a6c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -9,9 +9,6 @@ package com.facebook.react.uimanager; -import javax.annotation.Nullable; -import javax.annotation.concurrent.NotThreadSafe; - import android.content.res.Resources; import android.util.Log; import android.util.SparseArray; @@ -22,7 +19,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.PopupMenu; - import com.facebook.infer.annotation.Assertions; import com.facebook.react.animation.Animation; import com.facebook.react.animation.AnimationListener; @@ -39,6 +35,8 @@ import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController; import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; /** * Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between @@ -137,12 +135,7 @@ public class NativeViewHierarchyManager { } public synchronized void updateLayout( - int parentTag, - int tag, - int x, - int y, - int width, - int height) { + int parentTag, int tag, int x, int y, int width, int height) { UiThreadUtil.assertOnUiThread(); SystraceMessage.beginSection( Systrace.TRACE_TAG_REACT_VIEW, @@ -168,6 +161,19 @@ public class NativeViewHierarchyManager { View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); + // We update the layout of the ReactRootView when there is a change in the layout of its child. + // This is required to re-measure the size of the native View container (usually a + // FrameLayout) that is configured with layout_height = WRAP_CONTENT or layout_width = + // WRAP_CONTENT + // + // This code is going to be executed ONLY when there is a change in the size of the Root + // View defined in the js side. Changes in the layout of inner views will not trigger an update + // on the layour of the Root View. + ViewParent parent = viewToUpdate.getParent(); + if (parent instanceof RootView) { + parent.requestLayout(); + } + // Check if the parent of the view has to layout the view, or the child has to lay itself out. if (!mRootTags.get(parentTag)) { ViewManager parentViewManager = mTagsToViewManagers.get(parentTag); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index ea9f36642..0a90afd38 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -8,7 +8,12 @@ */ package com.facebook.react.uimanager; +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.UNSPECIFIED; + import android.os.SystemClock; +import android.view.View.MeasureSpec; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.animation.Animation; @@ -27,8 +32,10 @@ import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; import com.facebook.yoga.YogaDirection; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; /** @@ -37,6 +44,7 @@ import javax.annotation.Nullable; */ public class UIImplementation { + private final Set mMeasuredRootNodes = new HashSet<>(); private final ShadowNodeRegistry mShadowNodeRegistry = new ShadowNodeRegistry(); private final ViewManagerRegistry mViewManagers; private final UIViewOperationQueue mOperationsQueue; @@ -116,20 +124,66 @@ public class UIImplementation { } /** - * Registers a root node with a given tag, size and ThemedReactContext - * and adds it to a node registry. + * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by + * parameters. */ - public void registerRootView( - SizeMonitoringFrameLayout rootView, - int tag, - int width, - int height, - ThemedReactContext context) { + public void updateRootView(int tag, int widthMeasureSpec, int heightMeasureSpec) { + ReactShadowNode rootCSSNode = mShadowNodeRegistry.getNode(tag); + if (rootCSSNode == null) { + FLog.w(ReactConstants.TAG, "Tried to update non-existent root tag: " + tag); + return; + } + updateRootView(rootCSSNode, widthMeasureSpec, heightMeasureSpec); + } + + /** + * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by + * parameters. + */ + public void updateRootView( + ReactShadowNode rootCSSNode, int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + switch (widthMode) { + case EXACTLY: + rootCSSNode.setStyleWidth(widthSize); + break; + case AT_MOST: + rootCSSNode.setStyleMaxWidth(widthSize); + break; + case UNSPECIFIED: + rootCSSNode.setStyleWidthAuto(); + break; + } + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + switch (heightMode) { + case EXACTLY: + rootCSSNode.setStyleHeight(heightSize); + break; + case AT_MOST: + rootCSSNode.setStyleMaxHeight(heightSize); + break; + case UNSPECIFIED: + rootCSSNode.setStyleHeightAuto(); + break; + } + } + + /** + * Registers a root node with a given tag, size and ThemedReactContext and adds it to a node + * registry. + */ + public void registerRootView( + T rootView, int tag, ThemedReactContext context) { final ReactShadowNode rootCSSNode = createRootShadowNode(); rootCSSNode.setReactTag(tag); rootCSSNode.setThemedContext(context); - rootCSSNode.setStyleWidth(width); - rootCSSNode.setStyleHeight(height); + + int widthMeasureSpec = rootView.getWidthMeasureSpec(); + int heightMeasureSpec = rootView.getHeightMeasureSpec(); + updateRootView(rootCSSNode, widthMeasureSpec, heightMeasureSpec); mShadowNodeRegistry.addRootNode(rootCSSNode); @@ -583,27 +637,29 @@ public class UIImplementation { for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) { int tag = mShadowNodeRegistry.getRootTag(i); ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag); - SystraceMessage.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "UIImplementation.notifyOnBeforeLayoutRecursive") - .arg("rootTag", cssRoot.getReactTag()) - .flush(); - try { - notifyOnBeforeLayoutRecursive(cssRoot); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - calculateRootLayout(cssRoot); - SystraceMessage.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "UIImplementation.applyUpdatesRecursive") - .arg("rootTag", cssRoot.getReactTag()) - .flush(); - try { - applyUpdatesRecursive(cssRoot, 0f, 0f); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + if (mMeasuredRootNodes.contains(tag)) { + SystraceMessage.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "UIImplementation.notifyOnBeforeLayoutRecursive") + .arg("rootTag", cssRoot.getReactTag()) + .flush(); + try { + notifyOnBeforeLayoutRecursive(cssRoot); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + calculateRootLayout(cssRoot); + SystraceMessage.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIImplementation.applyUpdatesRecursive") + .arg("rootTag", cssRoot.getReactTag()) + .flush(); + try { + applyUpdatesRecursive(cssRoot, 0f, 0f); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } } } } finally { @@ -729,6 +785,7 @@ public class UIImplementation { private void removeShadowNodeRecursive(ReactShadowNode nodeToRemove) { NativeViewHierarchyOptimizer.handleRemoveNode(nodeToRemove); mShadowNodeRegistry.removeNode(nodeToRemove.getReactTag()); + mMeasuredRootNodes.remove(nodeToRemove.getReactTag()); for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { removeShadowNodeRecursive(nodeToRemove.getChildAt(i)); } @@ -906,4 +963,13 @@ public class UIImplementation { return rootTag; } + + /** + * Enables Layout calculation for a Root node that has been measured. + * + * @param rootViewTag {@link int} Tag of the root node + */ + public void enableLayoutCalculationForRootNode(int rootViewTag) { + this.mMeasuredRootNodes.add(rootViewTag); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 73c27377e..7fc14a632 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -173,36 +173,22 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements * Registers a new root view. JS can use the returned tag with manageChildren to add/remove * children to this view. * - * Note that this must be called after getWidth()/getHeight() actually return something. See + *

Note that this must be called after getWidth()/getHeight() actually return something. See * CatalystApplicationFragment as an example. * - * TODO(6242243): Make addRootView thread safe - * NB: this method is horribly not-thread-safe. + *

TODO(6242243): Make addRootView thread safe NB: this method is horribly not-thread-safe. */ - public int addRootView(final SizeMonitoringFrameLayout rootView) { + public int addRootView( + final T rootView) { Systrace.beginSection( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIManagerModule.addRootView"); final int tag = ReactRootViewTagGenerator.getNextRootViewTag(); - - final int width; - final int height; - // If LayoutParams sets size explicitly, we can use that. Otherwise get the size from the view. - if (rootView.getLayoutParams() != null && - rootView.getLayoutParams().width > 0 && - rootView.getLayoutParams().height > 0) { - width = rootView.getLayoutParams().width; - height = rootView.getLayoutParams().height; - } else { - width = rootView.getWidth(); - height = rootView.getHeight(); - } - final ReactApplicationContext reactApplicationContext = getReactApplicationContext(); final ThemedReactContext themedRootContext = new ThemedReactContext(reactApplicationContext, rootView.getContext()); - mUIImplementation.registerRootView(rootView, tag, width, height, themedRootContext); + mUIImplementation.registerRootView(rootView, tag, themedRootContext); rootView.setOnSizeChangedListener( new SizeMonitoringFrameLayout.OnSizeChangedListener() { @@ -594,8 +580,15 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements } /** - * Listener that drops the CSSNode pool on low memory when the app is backgrounded. + * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by + * parameters. */ + public void updateRootLayoutSpecs(int rootViewTag, int widthMeasureSpec, int heightMeasureSpec) { + mUIImplementation.updateRootView(rootViewTag, widthMeasureSpec, heightMeasureSpec); + mUIImplementation.dispatchViewUpdates(-1); + } + + /** Listener that drops the CSSNode pool on low memory when the app is backgrounded. */ private class MemoryTrimCallback implements ComponentCallbacks2 { @Override