diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 1efc5fe0890ca3..2819e7e2af1224 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -49,6 +49,7 @@ import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.JSTouchDispatcher; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactClippingProhibitedView; import com.facebook.react.uimanager.ReactRoot; import com.facebook.react.uimanager.RootView; import com.facebook.react.uimanager.RootViewUtil; @@ -329,9 +330,29 @@ private void removeOnGlobalLayoutListener() { } @Override - public void onViewAdded(View child) { + public void onViewAdded(final View child) { super.onViewAdded(child); + // See comments in {@code ReactRootViewProhibitedChildView} for why we want this mechanism. + if (child instanceof ReactClippingProhibitedView) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (!child.isShown()) { + ReactSoftException.logSoftException( + TAG, + new IllegalViewOperationException( + "A view was illegally added as a child of a ReactRootView. " + + "This View should not be a direct child of a ReactRootView, because it is not visible and will never be reachable. Child: " + + child.getClass().getCanonicalName().toString() + + " child ID: " + + child.getId())); + } + } + }); + } + if (mShouldLogContentAppeared) { mShouldLogContentAppeared = false; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingProhibitedView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingProhibitedView.java new file mode 100644 index 00000000000000..c6ff728e0a4679 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingProhibitedView.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager; + +/** + * Some Views may not function if added directly to a ViewGroup that clips them. For example, TTRC + * markers may rely on `onDraw` functionality to work properly, and will break if they're clipped + * out of the View hierarchy for any resaon. + * + *

This situation can occur more often in Fabric with View Flattening. We may prevent this sort + * of View Flattening from occurring in the future, but the connection is not entirely certain. + * + *

This can occur either because ReactViewGroup clips them out, using the ordinarary subview + * clipping feature. It is also possible if a View is added directly to a ReactRootView below the + * fold of the screen. + * + *

Generally the solution is to prevent View flattening in JS by adding `collapsable=false` to a + * parent component of the clipped view, and/or move the View higher up in the hierarchy so it is + * always rendered within the first page of the screen. + */ +public interface ReactClippingProhibitedView {} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 14529b4486e881..021bfeef758e65 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -29,6 +29,7 @@ import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactSoftException; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.modules.i18nmanager.I18nUtil; @@ -38,6 +39,7 @@ import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.ReactClippingProhibitedView; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ReactPointerEventsView; @@ -549,7 +551,7 @@ protected void dispatchSetPressed(boolean pressed) { } /*package*/ void addViewWithSubviewClippingEnabled( - View child, int index, ViewGroup.LayoutParams params) { + final View child, int index, ViewGroup.LayoutParams params) { Assertions.assertCondition(mRemoveClippedSubviews); Assertions.assertNotNull(mClippingRect); Assertions.assertNotNull(mAllChildren); @@ -564,6 +566,29 @@ protected void dispatchSetPressed(boolean pressed) { } updateSubviewClipStatus(mClippingRect, index, clippedSoFar); child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); + + if (child instanceof ReactClippingProhibitedView) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (!child.isShown()) { + ReactSoftException.logSoftException( + TAG, + new IllegalViewOperationException( + "Child view has been added to Parent view in which it is clipped and not visible." + + " This is not legal for this particular child view. Child: [" + + child.getId() + + "] " + + child.toString() + + " Parent: [" + + getId() + + "] " + + toString())); + } + } + }); + } } /*package*/ void removeViewWithSubviewClippingEnabled(View view) {