Add overflowInset to RN Android ViewGroup as separate mount instruction

Summary:
This diff adds `overflowInset` values in RN Android. These values are used to give  an enlarged boundary of a view group that also contains all its children layout. Here is [the post](https://fb.workplace.com/groups/yogalayout/permalink/2264980363573947/) that discuss more on why this is useful. I steal the pic in that post here as TLDR:

{F687030994}

In the above case, we will get overflowInset for ViewGroup A as something like `top: 0, right: -20, bottom: -20, left: 0`.

This has been added in the [Fabric core](https://fburl.com/code/f8c5tg7b) and [in IOS](https://fburl.com/code/vkh0hpt6). In Android, since we used to ignore all event coordinates outside of a ViewGroup boundary, this is not an issue. However, that caused unregistered touch area problem and got fixed in D30104853 (e35a963bfb), which dropped the boundary check and made the hit test algorithm in [TouchTargetHelper.java](https://fburl.com/code/dj8jiz22) worse as we now need to explore all the child node under ReactRootNode.

This perf issue is getting obvious when a view loads too many items, which matches our experience with "Hover getting slow after scrolling", "Hover getting slow after going back from PDP view", and "The saved list view (in Explore) is very fast (because it has very few components)"

To fix this issue, I added the support to `overflowInset` to RN Android by
1. Sending the `overflowInset` values from Binding.cpp in RN Android as a separate mount instruction
2. Update `IntBufferBatchMountItem.java` to read the int buffer sent over JNI, and pass the `overflowInset` values to `SurfaceMountingManager.java`
3. Creating new interface `ReactOverflowViewWithInset.java` and extending the existing `ReactOverflowView.java` usages
4. Adding implementation of getter and setter for `overflowInset` in various views
5. Update `TouchTargetHelper.java` to read the values and check boundaries before exploring ViewGroup's children

Note that in #3 I didn't change `ReactOverflowView.java` interface directly. I am concerned about backward compatibility issues in case this interface is being used in OSS. I suggest we deprecate it as we are not using it anymore in our code.

Changelog:
[Internal][Android]

Reviewed By: JoshuaGross

Differential Revision: D33133977

fbshipit-source-id: 64e3e837fe7ca6e6dbdbc836ab0615182e10f28c
This commit is contained in:
Xin Chen 2021-12-20 18:42:27 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 821382b9f7
Коммит bc9168d4ca
10 изменённых файлов: 211 добавлений и 10 удалений

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

@ -45,6 +45,10 @@ CppMountItem CppMountItem::UpdatePaddingMountItem(
ShadowView const &shadowView) {
return {CppMountItem::Type::UpdatePadding, {}, {}, shadowView, -1};
}
CppMountItem CppMountItem::UpdateOverflowInsetMountItem(
ShadowView const &shadowView) {
return {CppMountItem::Type::UpdateOverflowInset, {}, {}, shadowView, -1};
}
} // namespace react
} // namespace facebook

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

@ -45,6 +45,9 @@ struct CppMountItem final {
static CppMountItem UpdatePaddingMountItem(ShadowView const &shadowView);
static CppMountItem UpdateOverflowInsetMountItem(
ShadowView const &shadowView);
#pragma mark - Type
enum Type {
@ -58,7 +61,8 @@ struct CppMountItem final {
UpdateState = 64,
UpdateLayout = 128,
UpdateEventEmitter = 256,
UpdatePadding = 512
UpdatePadding = 512,
UpdateOverflowInset = 1024
};
#pragma mark - Fields

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

@ -43,6 +43,8 @@ static inline int getIntBufferSizeForType(CppMountItem::Type mountItemType) {
return 5; // tag, top, left, bottom, right
case CppMountItem::Type::UpdateLayout:
return 6; // tag, x, y, w, h, DisplayType
case CppMountItem::Type::UpdateOverflowInset:
return 5; // tag, left, top, right, bottom
case CppMountItem::Undefined:
case CppMountItem::Multiple:
return -1;
@ -84,6 +86,7 @@ static inline void computeBufferSizes(
std::vector<CppMountItem> &cppUpdateStateMountItems,
std::vector<CppMountItem> &cppUpdatePaddingMountItems,
std::vector<CppMountItem> &cppUpdateLayoutMountItems,
std::vector<CppMountItem> &cppUpdateOverflowInsetMountItems,
std::vector<CppMountItem> &cppUpdateEventEmitterMountItems) {
CppMountItem::Type lastType = CppMountItem::Type::Undefined;
int numSameType = 0;
@ -128,6 +131,11 @@ static inline void computeBufferSizes(
cppUpdateLayoutMountItems.size(),
batchMountItemIntsSize,
batchMountItemObjectsSize);
updateBufferSizes(
CppMountItem::Type::UpdateOverflowInset,
cppUpdateOverflowInsetMountItems.size(),
batchMountItemIntsSize,
batchMountItemObjectsSize);
updateBufferSizes(
CppMountItem::Type::UpdateEventEmitter,
cppUpdateEventEmitterMountItems.size(),
@ -232,6 +240,7 @@ void FabricMountingManager::executeMount(
std::vector<CppMountItem> cppUpdateStateMountItems;
std::vector<CppMountItem> cppUpdatePaddingMountItems;
std::vector<CppMountItem> cppUpdateLayoutMountItems;
std::vector<CppMountItem> cppUpdateOverflowInsetMountItems;
std::vector<CppMountItem> cppUpdateEventEmitterMountItems;
for (const auto &mutation : mutations) {
@ -293,6 +302,16 @@ void FabricMountingManager::executeMount(
CppMountItem::UpdateLayoutMountItem(
mutation.newChildShadowView));
}
// OverflowInset: This is the values indicating boundaries including
// children of the current view. The layout of current view may not
// change, and we separate this part from layout mount items to not
// pack too much data there.
if (oldChildShadowView.layoutMetrics.overflowInset !=
newChildShadowView.layoutMetrics.overflowInset) {
cppUpdateOverflowInsetMountItems.push_back(
CppMountItem::UpdateOverflowInsetMountItem(newChildShadowView));
}
}
if (oldChildShadowView.eventEmitter !=
@ -331,6 +350,13 @@ void FabricMountingManager::executeMount(
// Layout
cppUpdateLayoutMountItems.push_back(
CppMountItem::UpdateLayoutMountItem(mutation.newChildShadowView));
// OverflowInset: This is the values indicating boundaries including
// children of the current view. The layout of current view may not
// change, and we separate this part from layout mount items to not
// pack too much data there.
cppUpdateOverflowInsetMountItems.push_back(
CppMountItem::UpdateOverflowInsetMountItem(newChildShadowView));
}
// EventEmitter
@ -359,6 +385,7 @@ void FabricMountingManager::executeMount(
cppUpdateStateMountItems,
cppUpdatePaddingMountItems,
cppUpdateLayoutMountItems,
cppUpdateOverflowInsetMountItems,
cppUpdateEventEmitterMountItems);
static auto createMountItemsIntBufferBatchContainer =
@ -407,7 +434,7 @@ void FabricMountingManager::executeMount(
int intBufferPosition = 0;
int objBufferPosition = 0;
int prevMountItemType = -1;
jint temp[7];
jint temp[6];
for (int i = 0; i < cppCommonMountItems.size(); i++) {
const auto &mountItem = cppCommonMountItems[i];
const auto &mountItemType = mountItem.type;
@ -594,6 +621,36 @@ void FabricMountingManager::executeMount(
intBufferPosition += 6;
}
}
if (!cppUpdateOverflowInsetMountItems.empty()) {
writeIntBufferTypePreamble(
CppMountItem::Type::UpdateOverflowInset,
cppUpdateOverflowInsetMountItems.size(),
env,
intBufferArray,
intBufferPosition);
for (const auto &mountItem : cppUpdateOverflowInsetMountItems) {
auto layoutMetrics = mountItem.newChildShadowView.layoutMetrics;
auto pointScaleFactor = layoutMetrics.pointScaleFactor;
auto overflowInset = layoutMetrics.overflowInset;
int overflowInsetLeft =
round(scale(overflowInset.left, pointScaleFactor));
int overflowInsetTop = round(scale(overflowInset.top, pointScaleFactor));
int overflowInsetRight =
round(scale(overflowInset.right, pointScaleFactor));
int overflowInsetBottom =
round(scale(overflowInset.bottom, pointScaleFactor));
temp[0] = mountItem.newChildShadowView.tag;
temp[1] = overflowInsetLeft;
temp[2] = overflowInsetTop;
temp[3] = overflowInsetRight;
temp[4] = overflowInsetBottom;
env->SetIntArrayRegion(intBufferArray, intBufferPosition, 5, temp);
intBufferPosition += 5;
}
}
if (!cppUpdateEventEmitterMountItems.empty()) {
writeIntBufferTypePreamble(
CppMountItem::Type::UpdateEventEmitter,

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

@ -32,6 +32,7 @@ import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor;
import com.facebook.react.fabric.mounting.mountitems.MountItem;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.ReactOverflowViewWithInset;
import com.facebook.react.uimanager.ReactRoot;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.RootView;
@ -758,6 +759,35 @@ public class SurfaceMountingManager {
viewManager.setPadding(viewToUpdate, left, top, right, bottom);
}
@UiThread
public void updateOverflowInset(
int reactTag,
int overflowInsetLeft,
int overflowInsetTop,
int overflowInsetRight,
int overflowInsetBottom) {
if (isStopped()) {
return;
}
ViewState viewState = getViewState(reactTag);
// Do not layout Root Views
if (viewState.mIsRoot) {
return;
}
View viewToUpdate = viewState.mView;
if (viewToUpdate == null) {
throw new IllegalStateException("Unable to find View for tag: " + reactTag);
}
if (viewToUpdate instanceof ReactOverflowViewWithInset) {
((ReactOverflowViewWithInset) viewToUpdate)
.setOverflowInset(
overflowInsetLeft, overflowInsetTop, overflowInsetRight, overflowInsetBottom);
}
}
@UiThread
public void updateState(final int reactTag, @Nullable StateWrapper stateWrapper) {
UiThreadUtil.assertOnUiThread();

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

@ -49,6 +49,7 @@ public class IntBufferBatchMountItem implements MountItem {
static final int INSTRUCTION_UPDATE_LAYOUT = 128;
static final int INSTRUCTION_UPDATE_EVENT_EMITTER = 256;
static final int INSTRUCTION_UPDATE_PADDING = 512;
static final int INSTRUCTION_UPDATE_OVERFLOW_INSET = 1024;
private final int mSurfaceId;
private final int mCommitNumber;
@ -163,11 +164,25 @@ public class IntBufferBatchMountItem implements MountItem {
int width = mIntBuffer[i++];
int height = mIntBuffer[i++];
int displayType = mIntBuffer[i++];
surfaceMountingManager.updateLayout(reactTag, x, y, width, height, displayType);
} else if (type == INSTRUCTION_UPDATE_PADDING) {
surfaceMountingManager.updatePadding(
mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++], mIntBuffer[i++]);
} else if (type == INSTRUCTION_UPDATE_OVERFLOW_INSET) {
int reactTag = mIntBuffer[i++];
int overflowInsetLeft = mIntBuffer[i++];
int overflowInsetTop = mIntBuffer[i++];
int overflowInsetRight = mIntBuffer[i++];
int overflowInsetBottom = mIntBuffer[i++];
surfaceMountingManager.updateOverflowInset(
reactTag,
overflowInsetLeft,
overflowInsetTop,
overflowInsetRight,
overflowInsetBottom);
} else if (type == INSTRUCTION_UPDATE_EVENT_EMITTER) {
surfaceMountingManager.updateEventEmitter(
mIntBuffer[i++], castToEventEmitter(mObjBuffer[j++]));
@ -251,6 +266,15 @@ public class IntBufferBatchMountItem implements MountItem {
mIntBuffer[i++],
mIntBuffer[i++],
mIntBuffer[i++]));
} else if (type == INSTRUCTION_UPDATE_OVERFLOW_INSET) {
s.append(
String.format(
"UPDATE OVERFLOWINSET [%d]: left:%d top:%d right:%d bottom:%d\n",
mIntBuffer[i++],
mIntBuffer[i++],
mIntBuffer[i++],
mIntBuffer[i++],
mIntBuffer[i++]));
} else if (type == INSTRUCTION_UPDATE_EVENT_EMITTER) {
j += 1;
s.append(String.format("UPDATE EVENTEMITTER [%d]\n", mIntBuffer[i++]));

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

@ -0,0 +1,33 @@
/*
* 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;
import android.graphics.Rect;
import android.view.View;
/**
* Interface that should be implemented by {@link View} subclasses that support {@code overflow}
* style and want to use the overflowInset values. This allows the overflow information to be used
* by {@link TouchTargetHelper} to determine if a View is touchable.
*/
public interface ReactOverflowViewWithInset extends ReactOverflowView {
/**
* Get the overflow inset rect values which indicate the extensions to the boundaries of current
* view that wraps all of its children views
*
* @return Rect of integers indicating the left, top, right, bottom pixel extensions. The values
* are non-positive (indicating enlarged boundaries).
*/
Rect getOverflowInset();
/**
* Set the overflow inset rect values which indicate the extensions to the boundaries of current
* view that wraps all of its children views
*/
void setOverflowInset(int left, int top, int right, int bottom);
}

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

@ -182,8 +182,14 @@ public class TouchTargetHelper {
if (!isTouchPointInView(eventCoords[0], eventCoords[1], view)) {
// We don't allow touches on views that are outside the bounds of an `overflow: hidden` and
// `overflow: scroll` View.
if (view instanceof ReactOverflowView) {
@Nullable String overflow = ((ReactOverflowView) view).getOverflow();
if (view instanceof ReactOverflowViewWithInset) {
// If the touch point is outside of the overflowinset for the view, we can safely ignore
// it.
if (!isTouchPointInViewWithOverflowInset(eventCoords[0], eventCoords[1], view)) {
return null;
}
@Nullable String overflow = ((ReactOverflowViewWithInset) view).getOverflow();
if (ViewProps.HIDDEN.equals(overflow) || ViewProps.SCROLL.equals(overflow)) {
return null;
}
@ -253,6 +259,16 @@ public class TouchTargetHelper {
}
}
private static boolean isTouchPointInViewWithOverflowInset(float x, float y, View view) {
if (!(view instanceof ReactOverflowViewWithInset)) {
return false;
}
final Rect overflowInset = ((ReactOverflowViewWithInset) view).getOverflowInset();
return (x >= -overflowInset.left && x < view.getWidth() - overflowInset.right)
&& (y >= -overflowInset.top && y < view.getHeight() - overflowInset.bottom);
}
/**
* Returns the coordinates of a touch in the child View. It is transform aware and will invert the
* transform Matrix to find the true local points This code is taken from {@link

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

@ -42,7 +42,7 @@ import com.facebook.react.uimanager.FabricViewStateManager;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactOverflowView;
import com.facebook.react.uimanager.ReactOverflowViewWithInset;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasFlingAnimator;
@ -57,7 +57,7 @@ import java.util.List;
public class ReactHorizontalScrollView extends HorizontalScrollView
implements ReactClippingViewGroup,
FabricViewStateManager.HasFabricViewStateManager,
ReactOverflowView,
ReactOverflowViewWithInset,
HasScrollState,
HasFlingAnimator {
@ -77,6 +77,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
private final @Nullable OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect();
private final Rect mOverflowInset = new Rect();
private boolean mActivelyScrolling;
private @Nullable Rect mClippingRect;
@ -256,6 +257,16 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
return mOverflow;
}
@Override
public void setOverflowInset(int left, int top, int right, int bottom) {
mOverflowInset.set(left, top, right, bottom);
}
@Override
public Rect getOverflowInset() {
return mOverflowInset;
}
@Override
protected void onDraw(Canvas canvas) {
if (DEBUG_MODE) {

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

@ -38,7 +38,7 @@ import com.facebook.react.uimanager.FabricViewStateManager;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactOverflowView;
import com.facebook.react.uimanager.ReactOverflowViewWithInset;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasFlingAnimator;
@ -60,7 +60,7 @@ public class ReactScrollView extends ScrollView
ViewGroup.OnHierarchyChangeListener,
View.OnLayoutChangeListener,
FabricViewStateManager.HasFabricViewStateManager,
ReactOverflowView,
ReactOverflowViewWithInset,
HasScrollState,
HasFlingAnimator {
@ -73,6 +73,7 @@ public class ReactScrollView extends ScrollView
private final @Nullable OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect(); // for reuse to avoid allocation
private final Rect mOverflowInset = new Rect();
private boolean mActivelyScrolling;
private @Nullable Rect mClippingRect;
@ -231,6 +232,16 @@ public class ReactScrollView extends ScrollView
return mOverflow;
}
@Override
public void setOverflowInset(int left, int top, int right, int bottom) {
mOverflowInset.set(left, top, right, bottom);
}
@Override
public Rect getOverflowInset() {
return mOverflowInset;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);

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

@ -44,7 +44,7 @@ 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.ReactOverflowView;
import com.facebook.react.uimanager.ReactOverflowViewWithInset;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.ReactZIndexedViewGroup;
import com.facebook.react.uimanager.RootView;
@ -66,11 +66,12 @@ public class ReactViewGroup extends ViewGroup
ReactPointerEventsView,
ReactHitSlopView,
ReactZIndexedViewGroup,
ReactOverflowView {
ReactOverflowViewWithInset {
private static final int ARRAY_CAPACITY_INCREMENT = 12;
private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0);
private final Rect mOverflowInset = new Rect();
/* should only be used in {@link #updateClippingToRect} */
private static final Rect sHelperRect = new Rect();
@ -726,6 +727,16 @@ public class ReactViewGroup extends ViewGroup
return mOverflow;
}
@Override
public void setOverflowInset(int left, int top, int right, int bottom) {
mOverflowInset.set(left, top, right, bottom);
}
@Override
public Rect getOverflowInset() {
return mOverflowInset;
}
/**
* Set the background for the view or remove the background. It calls {@link
* #setBackground(Drawable)} or {@link #setBackgroundDrawable(Drawable)} based on the sdk version.