/** * 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.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.UiThreadUtil; /** * 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]; /** * Find touch event target view within the provided container given the coordinates provided * via {@link MotionEvent}. * * @param eventY the Y screen coordinate of the touch location * @param eventX the X 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 eventY, float eventX, ViewGroup viewGroup) { UiThreadUtil.assertOnUiThread(); int targetTag = viewGroup.getId(); // Store eventCoords in array so that they are modified to be relative to the targetView found. float[] eventCoords = mEventCoords; eventCoords[0] = eventY; eventCoords[1] = eventX; View nativeTargetView = findTouchTargetView(eventCoords, viewGroup); if (nativeTargetView != null) { View reactTargetView = findClosestReactAncestor(nativeTargetView); if (reactTargetView != null) { targetTag = getTouchTargetForView(reactTargetView, eventCoords[0], eventCoords[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); if (isTouchPointInView(eventCoords[0], eventCoords[1], viewGroup, child)) { // Apply offset to event coordinates to transform them into the coordinate space of the // child view, taken from {@link ViewGroup#dispatchTransformedTouchEvent()}. eventCoords[0] += viewGroup.getScrollY() - child.getTop(); eventCoords[1] += viewGroup.getScrollX() - child.getLeft(); View targetView = findTouchTargetViewWithPointerEvents(eventCoords, child); if (targetView != null) { return targetView; } eventCoords[0] -= viewGroup.getScrollY() - child.getTop(); eventCoords[1] -= viewGroup.getScrollX() - child.getLeft(); } } return viewGroup; } // Taken from {@link ViewGroup#isTransformedTouchPointInView()} private static boolean isTouchPointInView(float y, float x, ViewGroup parent, View child) { float localY = y + parent.getScrollY() - child.getTop(); float localX = x + parent.getScrollX() - child.getLeft(); // Taken from {@link View#pointInView()}. return localY >= 0 && localY < (child.getBottom() - child.getTop()) && localX >= 0 && localX < (child.getRight() - child.getLeft()); } /** * 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; 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); return targetView != view ? targetView : null; } return null; } else if (pointerEvents == PointerEvents.AUTO) { // Either this view or one of its children is the target 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 eventY, float eventX) { 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(); } }