Add support for dynamically sized ReactRootView

Reviewed By: achen1, AaaChiuuu

Differential Revision: D5745093

fbshipit-source-id: 65d85252ab8a0ca38322f49a3d4812380d5228c4
This commit is contained in:
David Vacca 2017-09-08 21:07:13 -07:00 committed by Facebook Github Bot
parent 1afc93d18a
commit 4ca617211b
5 changed files with 245 additions and 64 deletions

View File

@ -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

View File

@ -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();
}

View File

@ -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);

View File

@ -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<Integer> 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 <T extends SizeMonitoringFrameLayout & MeasureSpecProvider> 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);
}
}

View File

@ -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
* <p>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.
* <p>TODO(6242243): Make addRootView thread safe NB: this method is horribly not-thread-safe.
*/
public int addRootView(final SizeMonitoringFrameLayout rootView) {
public <T extends SizeMonitoringFrameLayout & MeasureSpecProvider> 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