/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.uimanager;
import javax.annotation.Nullable;
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 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();
for (int i = childrenCount - 1; i >= 0; i--) {
View child = viewGroup.getChildAt(i);
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();
}
}