Add mechanism to enforce certain Views always being visible in the View 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
This commit is contained in:
Joshua Gross 2021-06-30 10:38:41 -07:00 коммит произвёл Facebook GitHub Bot
Родитель e6b9508f12
Коммит 34903ba418
3 изменённых файлов: 74 добавлений и 2 удалений

Просмотреть файл

@ -49,6 +49,7 @@ import com.facebook.react.uimanager.DisplayMetricsHolder;
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 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
}
@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;

Просмотреть файл

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

Просмотреть файл

@ -29,6 +29,7 @@ import androidx.annotation.Nullable;
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.touch.ReactInterceptingViewGroup;
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 @@ public class ReactViewGroup extends ViewGroup
}
/*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 @@ public class ReactViewGroup extends ViewGroup
}
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) {