Skip to content

Commit

Permalink
Fix to make taps on views outside parent bounds work on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
hsource authored and pxpeterxu committed Jun 23, 2021
1 parent f33f209 commit b02efb1
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 62 deletions.
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;

import android.graphics.Rect;
import android.view.View;

import androidx.annotation.Nullable;

/**
* Interface that should be implemented by {@link View} subclasses that support {@code
* overflow} style. This allows the overflow information to be used by {@link TouchTargetHelper}
* to determine if a View is touchable.
*/
public interface ReactOverflowView {
/**
* Gets the overflow state of a view. If set, this should be one of {@link ViewProps#HIDDEN},
* {@link ViewProps#VISIBLE} or {@link ViewProps#SCROLL}.
*/
@Nullable String getOverflow();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.ReactHitSlopView;

import java.util.EnumSet;

/**
* Class responsible for identifying which react view should handle a given {@link MotionEvent}. It
* uses the event coordinates to traverse the view hierarchy and return a suitable view.
Expand Down Expand Up @@ -80,7 +82,7 @@ public static int findTargetTagAndCoordinatesForTouch(
// Store eventCoords in array so that they are modified to be relative to the targetView found.
viewCoords[0] = eventX;
viewCoords[1] = eventY;
View nativeTargetView = findTouchTargetView(viewCoords, viewGroup);
View nativeTargetView = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup);
if (nativeTargetView != null) {
View reactTargetView = findClosestReactAncestor(nativeTargetView);
if (reactTargetView != null) {
Expand All @@ -100,6 +102,20 @@ private static View findClosestReactAncestor(View view) {
return view;
}

/**
* Types of allowed return values from {@link #findTouchTargetView}.
*/
private enum TouchTargetReturnType {
/**
* Allow returning the view passed in through the parameters.
*/
SELF,
/**
* Allow returning children of the view passed in through parameters.
*/
CHILD,
}

/**
* Returns the touch target View that is either viewGroup or one if its descendants. This is a
* recursive DFS since view the entire tree must be parsed until the target is found. If the
Expand All @@ -111,18 +127,21 @@ private static View findClosestReactAncestor(View view) {
* be relative to the current viewGroup. When the method returns, it will contain the eventCoords
* relative to the targetView found.
*/
private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup) {
int childrenCount = viewGroup.getChildCount();
// Consider z-index when determining the touch target.
ReactZIndexedViewGroup zIndexedViewGroup =
private static View findTouchTargetView(
float[] eventCoords, View view, EnumSet<TouchTargetReturnType> allowReturnTouchTargetTypes) {
if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.CHILD)
&& view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childrenCount = viewGroup.getChildCount();
// Consider z-index when determining the touch target.
ReactZIndexedViewGroup zIndexedViewGroup =
viewGroup instanceof ReactZIndexedViewGroup ? (ReactZIndexedViewGroup) viewGroup : null;
for (int i = childrenCount - 1; i >= 0; i--) {
int childIndex =
for (int i = childrenCount - 1; i >= 0; i--) {
int childIndex =
zIndexedViewGroup != null ? zIndexedViewGroup.getZIndexMappedChildIndex(i) : i;
View child = viewGroup.getChildAt(childIndex);
PointF childPoint = mTempPoint;
if (isTransformedTouchPointInView(
eventCoords[0], eventCoords[1], viewGroup, child, childPoint)) {
View child = viewGroup.getChildAt(childIndex);
PointF childPoint = mTempPoint;
getChildPoint(eventCoords[0], eventCoords[1], viewGroup, child, childPoint);
// If it is contained within the child View, the childPoint value will contain the view
// coordinates relative to the child
// We need to store the existing X,Y for the viewGroup away as it is possible this child
Expand All @@ -132,22 +151,66 @@ private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup
eventCoords[0] = childPoint.x;
eventCoords[1] = childPoint.y;
View targetView = findTouchTargetViewWithPointerEvents(eventCoords, child);

if (targetView != null) {
return targetView;
// We don't allow touches on views that are outside the bounds of an `overflow: hidden`
// View
boolean inOverflowBounds = true;
if (viewGroup instanceof ReactOverflowView) {
@Nullable String overflow = ((ReactOverflowView) viewGroup).getOverflow();
if (ViewProps.HIDDEN.equals(overflow)
&& !isTouchPointInView(restoreX, restoreY, view)) {
inOverflowBounds = false;
}
}
if (inOverflowBounds) {
return targetView;
}
}
eventCoords[0] = restoreX;
eventCoords[1] = restoreY;
}
}
return viewGroup;

if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.SELF)
&& isTouchPointInView(eventCoords[0], eventCoords[1], view)) {
return view;
}

return null;
}

/**
* Checks whether a touch at {@code x} and {@code y} are within the bounds of the View. Both
* {@code x} and {@code y} must be relative to the top-left corner of the view.
*/
private static boolean isTouchPointInView(float x, float y, View view) {
if (view instanceof ReactHitSlopView && ((ReactHitSlopView) view).getHitSlopRect() != null) {
Rect hitSlopRect = ((ReactHitSlopView) view).getHitSlopRect();
if ((x >= -hitSlopRect.left
&& x < (view.getRight() - view.getLeft()) + hitSlopRect.right)
&& (y >= -hitSlopRect.top
&& y < (view.getBottom() - view.getTop()) + hitSlopRect.bottom)) {
return true;
}

return false;
} else {
if ((x >= 0 && x < (view.getRight() - view.getLeft()))
&& (y >= 0 && y < (view.getBottom() - view.getTop()))) {
return true;
}

return false;
}
}

/**
* Returns whether the touch point is within the child View It is transform aware and will invert
* 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
* ViewGroup#isTransformedTouchPointInView()}
*/
private static boolean isTransformedTouchPointInView(
private static void getChildPoint(
float x, float y, ViewGroup parent, View child, PointF outLocalPoint) {
float localX = x + parent.getScrollX() - child.getLeft();
float localY = y + parent.getScrollY() - child.getTop();
Expand All @@ -162,26 +225,7 @@ private static boolean isTransformedTouchPointInView(
localX = localXY[0];
localY = localXY[1];
}
if (child instanceof ReactHitSlopView && ((ReactHitSlopView) child).getHitSlopRect() != null) {
Rect hitSlopRect = ((ReactHitSlopView) child).getHitSlopRect();
if ((localX >= -hitSlopRect.left
&& localX < (child.getRight() - child.getLeft()) + hitSlopRect.right)
&& (localY >= -hitSlopRect.top
&& localY < (child.getBottom() - child.getTop()) + hitSlopRect.bottom)) {
outLocalPoint.set(localX, localY);
return true;
}

return false;
} else {
if ((localX >= 0 && localX < (child.getRight() - child.getLeft()))
&& (localY >= 0 && localY < (child.getBottom() - child.getTop()))) {
outLocalPoint.set(localX, localY);
return true;
}

return false;
}
outLocalPoint.set(localX, localY);
}

/**
Expand Down Expand Up @@ -211,32 +255,32 @@ private static boolean isTransformedTouchPointInView(
return null;

} else if (pointerEvents == PointerEvents.BOX_ONLY) {
// This view is the target, its children don't matter
return view;
// This view may be the target, its children don't matter
return findTouchTargetView(eventCoords, view, EnumSet.of(TouchTargetReturnType.SELF));

} else if (pointerEvents == PointerEvents.BOX_NONE) {
// This view can't be the target, but its children might.
if (view instanceof ViewGroup) {
View targetView = findTouchTargetView(eventCoords, (ViewGroup) view);
if (targetView != view) {
return targetView;
}
View targetView =
findTouchTargetView(eventCoords, view, EnumSet.of(TouchTargetReturnType.CHILD));
if (targetView != null) {
return targetView;
}

// PointerEvents.BOX_NONE means that this react element cannot receive pointer events.
// However, there might be virtual children that can receive pointer events, in which case
// we still want to return this View and dispatch a pointer event to the virtual element.
// Note that this currently only applies to Nodes/FlatViewGroup as it's the only class that
// is both a ViewGroup and ReactCompoundView (ReactTextView is a ReactCompoundView but not a
// ViewGroup).
if (view instanceof ReactCompoundView) {
int reactTag =
((ReactCompoundView) view).reactTagForTouch(eventCoords[0], eventCoords[1]);
if (reactTag != view.getId()) {
// make sure we exclude the View itself because of the PointerEvents.BOX_NONE
return view;
}
// PointerEvents.BOX_NONE means that this react element cannot receive pointer events.
// However, there might be virtual children that can receive pointer events, in which case
// we still want to return this View and dispatch a pointer event to the virtual element.
// Note that this currently only applies to Nodes/FlatViewGroup as it's the only class that
// is both a ViewGroup and ReactCompoundView (ReactTextView is a ReactCompoundView but not a
// ViewGroup).
if (view instanceof ReactCompoundView) {
int reactTag =
((ReactCompoundView) view).reactTagForTouch(eventCoords[0], eventCoords[1]);
if (reactTag != view.getId()) {
// make sure we exclude the View itself because of the PointerEvents.BOX_NONE
return view;
}
}

return null;

} else if (pointerEvents == PointerEvents.AUTO) {
Expand All @@ -246,10 +290,8 @@ private static boolean isTransformedTouchPointInView(
return view;
}
}
if (view instanceof ViewGroup) {
return findTouchTargetView(eventCoords, (ViewGroup) view);
}
return view;
return findTouchTargetView(eventCoords, view,
EnumSet.of(TouchTargetReturnType.SELF, TouchTargetReturnType.CHILD));

} else {
throw new JSApplicationIllegalArgumentException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactOverflowView;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.view.ReactViewBackgroundManager;
Expand All @@ -50,7 +51,9 @@

/** Similar to {@link ReactScrollView} but only supports horizontal scrolling. */
public class ReactHorizontalScrollView extends HorizontalScrollView
implements ReactClippingViewGroup, FabricViewStateManager.HasFabricViewStateManager {
implements ReactClippingViewGroup,
ReactOverflowView,
FabricViewStateManager.HasFabricViewStateManager {

private static @Nullable Field sScrollerField;
private static boolean sTriedToGetScrollerField = false;
Expand Down Expand Up @@ -233,6 +236,11 @@ public void setOverflow(String overflow) {
invalidate();
}

@Override
public @Nullable String getOverflow() {
return mOverflow;
}

@Override
protected void onDraw(Canvas canvas) {
getDrawingRect(mRect);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactOverflowView;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.view.ReactViewBackgroundManager;
Expand All @@ -53,6 +54,7 @@ public class ReactScrollView extends ScrollView
implements ReactClippingViewGroup,
ViewGroup.OnHierarchyChangeListener,
View.OnLayoutChangeListener,
ReactOverflowView,
FabricViewStateManager.HasFabricViewStateManager {

private static @Nullable Field sScrollerField;
Expand Down Expand Up @@ -205,6 +207,11 @@ public void setOverflow(String overflow) {
invalidate();
}

@Override
public @Nullable String getOverflow() {
return mOverflow;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactOverflowView;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.ReactZIndexedViewGroup;
import com.facebook.react.uimanager.RootView;
Expand All @@ -62,7 +63,8 @@ public class ReactViewGroup extends ViewGroup
ReactClippingViewGroup,
ReactPointerEventsView,
ReactHitSlopView,
ReactZIndexedViewGroup {
ReactZIndexedViewGroup,
ReactOverflowView {

private static final int ARRAY_CAPACITY_INCREMENT = 12;
private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
Expand Down Expand Up @@ -685,6 +687,7 @@ public void setOverflow(String overflow) {
invalidate();
}

@Override
public @Nullable String getOverflow() {
return mOverflow;
}
Expand Down
Loading

0 comments on commit b02efb1

Please sign in to comment.