Files
react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java
T
David Vacca aa5edca0e2 Migrate Nullable and NonNull annotations to AndroidX
Summary:
This diff migrates the usages Nullable and NonNull annotations to AndroidX instead of javax.

The purpose of this change is to bring consistency in the annotations used by the core of RN

Reviewed By: makovkastar

Differential Revision: D16054504

fbshipit-source-id: 21d888854da088d2a14615a90d4dc058e5286b91
2019-07-11 16:23:29 -07:00

268 lines
11 KiB
Java

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>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.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.ReactHitSlopView;
/**
* 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.
*/
public class TouchTargetHelper {
private static final float[] mEventCoords = new float[2];
private static final PointF mTempPoint = new PointF();
private static final float[] mMatrixTransformCoords = new float[2];
private static final Matrix mInverseMatrix = new Matrix();
/**
* Find touch event target view within the provided container given the coordinates provided via
* {@link MotionEvent}.
*
* @param eventX the X screen coordinate of the touch location
* @param eventY the Y screen coordinate of the touch location
* @param viewGroup the container view to traverse
* @return the react tag ID of the child view that should handle the event
*/
public static int findTargetTagForTouch(float eventX, float eventY, ViewGroup viewGroup) {
return findTargetTagAndCoordinatesForTouch(eventX, eventY, viewGroup, mEventCoords, null);
}
/**
* Find touch event target view within the provided container given the coordinates provided via
* {@link MotionEvent}.
*
* @param eventX the X screen coordinate of the touch location
* @param eventY the Y screen coordinate of the touch location
* @param viewGroup the container view to traverse
* @param nativeViewId the native react view containing this touch target
* @return the react tag ID of the child view that should handle the event
*/
public static int findTargetTagForTouch(
float eventX, float eventY, ViewGroup viewGroup, @Nullable int[] nativeViewId) {
return findTargetTagAndCoordinatesForTouch(
eventX, eventY, viewGroup, mEventCoords, nativeViewId);
}
/**
* Find touch event target view within the provided container given the coordinates provided via
* {@link MotionEvent}.
*
* @param eventX the X screen coordinate of the touch location
* @param eventY the Y screen coordinate of the touch location
* @param viewGroup the container view to traverse
* @param viewCoords an out parameter that will return the X,Y value in the target view
* @param nativeViewTag an out parameter that will return the native view id
* @return the react tag ID of the child view that should handle the event
*/
public static int findTargetTagAndCoordinatesForTouch(
float eventX,
float eventY,
ViewGroup viewGroup,
float[] viewCoords,
@Nullable int[] nativeViewTag) {
UiThreadUtil.assertOnUiThread();
int targetTag = viewGroup.getId();
// 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);
if (nativeTargetView != null) {
View reactTargetView = findClosestReactAncestor(nativeTargetView);
if (reactTargetView != null) {
if (nativeViewTag != null) {
nativeViewTag[0] = reactTargetView.getId();
}
targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]);
}
}
return targetTag;
}
private static View findClosestReactAncestor(View view) {
while (view != null && view.getId() <= 0) {
view = (View) view.getParent();
}
return view;
}
/**
* 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
* search does not backtrack, it is possible to follow a branch that cannot be a target (because
* of pointerEvents). For example, if both C and E can be the target of an event: A
* (pointerEvents: auto) - B (pointerEvents: box-none) - C (pointerEvents: none) \ D
* (pointerEvents: auto) - E (pointerEvents: auto) If the search goes down the first branch, it
* would return A as the target, which is incorrect. NB: This modifies the eventCoords to always
* 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 =
viewGroup instanceof ReactZIndexedViewGroup ? (ReactZIndexedViewGroup) viewGroup : null;
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)) {
// 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
// will not actually be the target and so we restore them if not
float restoreX = eventCoords[0];
float restoreY = eventCoords[1];
eventCoords[0] = childPoint.x;
eventCoords[1] = childPoint.y;
View targetView = findTouchTargetViewWithPointerEvents(eventCoords, child);
if (targetView != null) {
return targetView;
}
eventCoords[0] = restoreX;
eventCoords[1] = restoreY;
}
}
return viewGroup;
}
/**
* Returns whether the touch point is within 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(
float x, float y, ViewGroup parent, View child, PointF outLocalPoint) {
float localX = x + parent.getScrollX() - child.getLeft();
float localY = y + parent.getScrollY() - child.getTop();
Matrix matrix = child.getMatrix();
if (!matrix.isIdentity()) {
float[] localXY = mMatrixTransformCoords;
localXY[0] = localX;
localXY[1] = localY;
Matrix inverseMatrix = mInverseMatrix;
matrix.invert(inverseMatrix);
inverseMatrix.mapPoints(localXY);
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;
}
}
/**
* Returns the touch target View of the event given, or null if neither the given View nor any of
* its descendants are the touch target.
*/
private static @Nullable View findTouchTargetViewWithPointerEvents(
float eventCoords[], View view) {
PointerEvents pointerEvents =
view instanceof ReactPointerEventsView
? ((ReactPointerEventsView) view).getPointerEvents()
: PointerEvents.AUTO;
// Views that are disabled should never be the target of pointer events. However, their children
// can be because some views (SwipeRefreshLayout) use enabled but still have children that can
// be valid targets.
if (!view.isEnabled()) {
if (pointerEvents == PointerEvents.AUTO) {
pointerEvents = PointerEvents.BOX_NONE;
} else if (pointerEvents == PointerEvents.BOX_ONLY) {
pointerEvents = PointerEvents.NONE;
}
}
if (pointerEvents == PointerEvents.NONE) {
// This view and its children can't be the target
return null;
} else if (pointerEvents == PointerEvents.BOX_ONLY) {
// This view is the target, its children don't matter
return view;
} 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;
}
// 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) {
// Either this view or one of its children is the target
if (view instanceof ReactCompoundViewGroup) {
if (((ReactCompoundViewGroup) view).interceptsTouchEvent(eventCoords[0], eventCoords[1])) {
return view;
}
}
if (view instanceof ViewGroup) {
return findTouchTargetView(eventCoords, (ViewGroup) view);
}
return view;
} else {
throw new JSApplicationIllegalArgumentException(
"Unknown pointer event type: " + pointerEvents.toString());
}
}
private static int getTouchTargetForView(View targetView, float eventX, float eventY) {
if (targetView instanceof ReactCompoundView) {
// Use coordinates relative to the view, which have been already computed by
// {@link #findTouchTargetView()}.
return ((ReactCompoundView) targetView).reactTagForTouch(eventX, eventY);
}
return targetView.getId();
}
}