Skip to content

Commit

Permalink
Add mechanism to enforce certain Views always being visible in the Vi…
Browse files Browse the repository at this point in the history
…ew hierarchy

Summary:
See comments in ReactClippingProhibitedView for details and motivation behind this new feature.

You may have a View class inherit from the ReactClippingProhibitedView interface in order to enable this feature for instances of that View type.

This can be added to Views that should /never/ be clipped from the View hierarchy - namely, TTRC components or other telemetry components that always need to be rendered in order for some feature to function.

Changelog: [Added] Opt-in mechanism to allow native Android Views to be marked as "not clippable", soft exceptions will be logged if these Views are clipped from the View hierarchy

Reviewed By: sshic

Differential Revision: D29472439

fbshipit-source-id: b3be53df836b452aed5dc40514ff585ce0ad812b
  • Loading branch information
JoshuaGross authored and facebook-github-bot committed Jun 30, 2021
1 parent e6b9508 commit 34903ba
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 2 deletions.
23 changes: 22 additions & 1 deletion ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down

0 comments on commit 34903ba

Please sign in to comment.