diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 6c6be1bcb07..0d5f89d368f 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4424,18 +4424,18 @@ public class com/facebook/react/uimanager/ThemedReactContext : com/facebook/reac public fun removeLifecycleEventListener (Lcom/facebook/react/bridge/LifecycleEventListener;)V } -public class com/facebook/react/uimanager/TouchTargetHelper { - public fun ()V - public static fun findTargetPathAndCoordinatesForTouch (FFLandroid/view/ViewGroup;[F)Ljava/util/List; - public static fun findTargetTagAndCoordinatesForTouch (FFLandroid/view/ViewGroup;[F[I)I - public static fun findTargetTagForTouch (FFLandroid/view/ViewGroup;)I - public static fun findTargetTagForTouch (FFLandroid/view/ViewGroup;[I)I +public final class com/facebook/react/uimanager/TouchTargetHelper { + public static final field INSTANCE Lcom/facebook/react/uimanager/TouchTargetHelper; + public static final fun findTargetPathAndCoordinatesForTouch (FFLandroid/view/ViewGroup;[F)Ljava/util/List; + public static final fun findTargetTagAndCoordinatesForTouch (FFLandroid/view/ViewGroup;[F[I)I + public static final fun findTargetTagForTouch (FFLandroid/view/ViewGroup;)I + public static final fun findTargetTagForTouch (FFLandroid/view/ViewGroup;[I)I } -public class com/facebook/react/uimanager/TouchTargetHelper$ViewTarget { +public final class com/facebook/react/uimanager/TouchTargetHelper$ViewTarget { public fun equals (Ljava/lang/Object;)Z - public fun getView ()Landroid/view/View; - public fun getViewId ()I + public final fun getView ()Landroid/view/View; + public final fun getViewId ()I public fun hashCode ()I } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java deleted file mode 100644 index 46aecad0ef2..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and 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 static com.facebook.react.uimanager.common.UIManagerType.FABRIC; - -import android.annotation.SuppressLint; -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.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.touch.ReactHitSlopView; -import com.facebook.react.uimanager.common.ViewUtil; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; - -/** - * 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. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -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 = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup, null); - 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; - } - - /** - * 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 - * @return If a target was found, returns a {@link Lis} containing the path through - * the view tree of all react tags and views that are a container for the touch target, - * ordered from target to root (last element) - */ - @SuppressLint("ResourceType") - public static List findTargetPathAndCoordinatesForTouch( - float eventX, float eventY, ViewGroup viewGroup, float[] viewCoords) { - UiThreadUtil.assertOnUiThread(); - - // Store eventCoords in array so that they are modified to be relative to the targetView found. - viewCoords[0] = eventX; - viewCoords[1] = eventY; - - List pathAccumulator = new ArrayList<>(); - View targetView = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup, pathAccumulator); - - if (targetView != null) { - View reactTargetView = targetView; - int firstReactAncestor = 0; - // Same logic as findClosestReactAncestor but also track the index - while (reactTargetView != null && reactTargetView.getId() <= 0) { - reactTargetView = (View) reactTargetView.getParent(); - firstReactAncestor++; - } - - if (firstReactAncestor > 0 && firstReactAncestor <= pathAccumulator.size()) { - // Drop non-React views from the path trace - pathAccumulator = pathAccumulator.subList(firstReactAncestor, pathAccumulator.size()); - } - - if (reactTargetView != null) { - int targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]); - if (targetTag != reactTargetView.getId()) { - pathAccumulator.add(0, new ViewTarget(targetTag, (View) null)); - } - } - } - - return pathAccumulator; - } - - @SuppressLint("ResourceType") - private static @Nullable View findClosestReactAncestor(View view) { - while (view != null && view.getId() <= 0) { - view = (View) view.getParent(); - } - 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 - * 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 @Nullable View findTouchTargetView( - float[] eventCoords, - View view, - EnumSet allowReturnTouchTargetTypes, - @Nullable List pathAccumulator) { - // We prefer returning a child, so we check for a child that can handle the touch first - if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.CHILD) - && view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - 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 ReactOverflowViewWithInset) { - // If the touch point is outside of the overflowinset for the view, we can safely ignore - // it. - if (ViewUtil.getUIManagerType(view.getId()) == FABRIC - && !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; - } - } - - // We don't allow touches on views that are outside the bounds and has clipChildren set to - // true. - if (viewGroup.getClipChildren()) { - return null; - } - } - - 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; - getChildPoint(eventCoords[0], eventCoords[1], viewGroup, child, childPoint); - // 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, pathAccumulator); - if (targetView != null) { - return targetView; - } - eventCoords[0] = restoreX; - eventCoords[1] = restoreY; - } - } - - // Check if parent can handle the touch after the children - 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.getWidth()) + hitSlopRect.right) - && (y >= -hitSlopRect.top && y < (view.getHeight()) + hitSlopRect.bottom)) { - return true; - } - - return false; - } else { - if ((x >= 0 && x < (view.getWidth())) && (y >= 0 && y < (view.getHeight()))) { - return true; - } - - return false; - } - } - - 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 - * ViewGroup#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(); - 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]; - } - outLocalPoint.set(localX, localY); - } - - /** - * 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, @Nullable List pathAccumulator) { - 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 may be the target, its children don't matter - View targetView = - findTouchTargetView( - eventCoords, view, EnumSet.of(TouchTargetReturnType.SELF), pathAccumulator); - if (targetView != null && pathAccumulator != null) { - pathAccumulator.add(new ViewTarget(view.getId(), view)); - } - return targetView; - - } else if (pointerEvents == PointerEvents.BOX_NONE) { - // This view can't be the target, but its children might. - View targetView = - findTouchTargetView( - eventCoords, view, EnumSet.of(TouchTargetReturnType.CHILD), pathAccumulator); - if (targetView != null) { - if (pathAccumulator != null) { - pathAccumulator.add(new ViewTarget(view.getId(), 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 - && isTouchPointInView(eventCoords[0], eventCoords[1], view)) { - int reactTag = ((ReactCompoundView) view).reactTagForTouch(eventCoords[0], eventCoords[1]); - // make sure we exclude the View itself because of the PointerEvents.BOX_NONE - if (reactTag != view.getId()) { - if (pathAccumulator != null) { - pathAccumulator.add(new ViewTarget(view.getId(), view)); - } - return view; - } - } - - return null; - - } else { - if (pointerEvents != PointerEvents.AUTO) { - FLog.w(ReactConstants.TAG, "Unknown pointer event type: " + pointerEvents.toString()); - } - // Either this view or one of its children is the target - if (view instanceof ReactCompoundViewGroup - && isTouchPointInView(eventCoords[0], eventCoords[1], view) - && ((ReactCompoundViewGroup) view).interceptsTouchEvent(eventCoords[0], eventCoords[1])) { - if (pathAccumulator != null) { - pathAccumulator.add(new ViewTarget(view.getId(), view)); - } - return view; - } - - View result = - findTouchTargetView( - eventCoords, - view, - EnumSet.of(TouchTargetReturnType.SELF, TouchTargetReturnType.CHILD), - pathAccumulator); - if (result != null && pathAccumulator != null) { - pathAccumulator.add(new ViewTarget(view.getId(), view)); - } - return result; - } - } - - private static int getTouchTargetForView(View targetView, float viewX, float viewY) { - if (targetView instanceof ReactCompoundView) { - // Use coordinates relative to the view, which have been already computed by - // {@link #findTouchTargetView()}. - return ((ReactCompoundView) targetView).reactTagForTouch(viewX, viewY); - } - return targetView.getId(); - } - - public static class ViewTarget { - private final int mViewId; - private final @Nullable View mView; - - private ViewTarget(int viewId, @Nullable View view) { - mViewId = viewId; - mView = view; - } - - public int getViewId() { - return mViewId; - } - - @Nullable - public View getView() { - return mView; - } - - @Override - public boolean equals(@Nullable Object o) { - // If the object is compared with itself then return true - if (o == this) { - return true; - } - - // Check if o is an instance of ViewTarget. Note that "null instanceof ViewTarget" also - // returns false. - if (!(o instanceof ViewTarget)) { - return false; - } - - ViewTarget other = (ViewTarget) o; - // We only need to compare view id, as we assume the same view id will always map to the same - // view. TargetView is not mutable so this should be safe. - return other.getViewId() == mViewId; - } - - @Override - public int hashCode() { - return Objects.hashCode(mViewId); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.kt new file mode 100644 index 00000000000..80fdb1fc8e9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.kt @@ -0,0 +1,435 @@ +/* + * Copyright (c) Meta Platforms, Inc. and 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.annotation.SuppressLint +import android.graphics.Matrix +import android.graphics.PointF +import android.view.View +import android.view.ViewGroup +import com.facebook.common.logging.FLog +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.ReactConstants +import com.facebook.react.touch.ReactHitSlopView +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.common.ViewUtil +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. + */ +public object TouchTargetHelper { + + private val eventCoords = FloatArray(2) + private val tempPoint = PointF() + private val matrixTransformCoords = FloatArray(2) + private val inverseMatrix = 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 + */ + @JvmStatic + public fun findTargetTagForTouch(eventX: Float, eventY: Float, viewGroup: ViewGroup): Int = + findTargetTagAndCoordinatesForTouch(eventX, eventY, viewGroup, eventCoords, 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 + */ + @JvmStatic + public fun findTargetTagForTouch( + eventX: Float, + eventY: Float, + viewGroup: ViewGroup, + nativeViewId: IntArray? + ): Int = findTargetTagAndCoordinatesForTouch(eventX, eventY, viewGroup, eventCoords, 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 + */ + @JvmStatic + public fun findTargetTagAndCoordinatesForTouch( + eventX: Float, + eventY: Float, + viewGroup: ViewGroup, + viewCoords: FloatArray, + nativeViewTag: IntArray? + ): Int { + UiThreadUtil.assertOnUiThread() + var targetTag = viewGroup.id + // Store eventCoords in array so that they are modified to be relative to the targetView found. + viewCoords[0] = eventX + viewCoords[1] = eventY + val nativeTargetView = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup, null) + if (nativeTargetView != null) { + val reactTargetView = findClosestReactAncestor(nativeTargetView) + if (reactTargetView != null) { + nativeViewTag?.let { nativeViewTag[0] = reactTargetView.id } + targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]) + } + } + return targetTag + } + + /** + * 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 + * @return If a target was found, returns a {@link List} containing the path through + * the view tree of all react tags and views that are a container for the touch target, ordered + * from target to root (last element) + */ + @SuppressLint("ResourceType") + @JvmStatic + public fun findTargetPathAndCoordinatesForTouch( + eventX: Float, + eventY: Float, + viewGroup: ViewGroup, + viewCoords: FloatArray + ): List { + UiThreadUtil.assertOnUiThread() + + // Store eventCoords in array so that they are modified to be relative to the targetView found. + viewCoords[0] = eventX + viewCoords[1] = eventY + + val pathAccumulator = mutableListOf() + val targetView = findTouchTargetViewWithPointerEvents(viewCoords, viewGroup, pathAccumulator) + + if (targetView != null) { + var reactTargetView: View? = targetView + var firstReactAncestor = 0 + // Same logic as findClosestReactAncestor but also track the index + while (reactTargetView != null && reactTargetView.id <= 0) { + reactTargetView = reactTargetView.parent as? View + firstReactAncestor++ + } + + if (firstReactAncestor > 0 && firstReactAncestor <= pathAccumulator.size) { + // Drop non-React views from the path trace + pathAccumulator.subList(firstReactAncestor, pathAccumulator.size) + } + + if (reactTargetView != null) { + val targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]) + if (targetTag != reactTargetView.id) { + pathAccumulator.add(0, ViewTarget(targetTag, null)) + } + } + } + + return pathAccumulator + } + + @SuppressLint("ResourceType") + private fun findClosestReactAncestor(view: View?): View? { + var currentView = view + while (currentView != null && currentView.id <= 0) { + currentView = currentView.parent as? View + } + return currentView + } + + private enum class 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 of its descendants. This is a + * recursive DFS since 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 fun findTouchTargetView( + eventCoords: FloatArray, + view: View, + allowReturnTouchTargetTypes: EnumSet, + pathAccumulator: MutableList? + ): View? { + // We prefer returning a child, so we check for a child that can handle the touch first + if (allowReturnTouchTargetTypes.contains(TouchTargetReturnType.CHILD) && view is ViewGroup) { + val viewGroup = view + 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 is ReactOverflowViewWithInset) { + // If the touch point is outside of the overflow inset for the view, we can safely ignore + // it. + if (ViewUtil.getUIManagerType(view.id) == UIManagerType.FABRIC && + !isTouchPointInViewWithOverflowInset(eventCoords[0], eventCoords[1], view)) { + return null + } + + val overflow = view.overflow + if (ViewProps.HIDDEN == overflow || ViewProps.SCROLL == overflow) { + return null + } + } + + // We don't allow touches on views that are outside the bounds and has clipChildren set to + // true. + if (viewGroup.clipChildren) { + return null + } + } + + val childrenCount = viewGroup.childCount + // Consider z-index when determining the touch target. + val zIndexedViewGroup = viewGroup as? ReactZIndexedViewGroup + for (i in childrenCount - 1 downTo 0) { + val childIndex = zIndexedViewGroup?.getZIndexMappedChildIndex(i) ?: i + val child = viewGroup.getChildAt(childIndex) + val childPoint = tempPoint + getChildPoint(eventCoords[0], eventCoords[1], viewGroup, child, childPoint) + // 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 + val restoreX = eventCoords[0] + val restoreY = eventCoords[1] + eventCoords[0] = childPoint.x + eventCoords[1] = childPoint.y + val target = findTouchTargetViewWithPointerEvents(eventCoords, child, pathAccumulator) + if (target != null) { + return target + } + eventCoords[0] = restoreX + eventCoords[1] = restoreY + } + } + + // Check if parent can handle the touch after the children + 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 fun isTouchPointInView(x: Float, y: Float, view: View): Boolean { + val hitSlopRect = (view as? ReactHitSlopView)?.hitSlopRect + if (hitSlopRect != null) { + if (x >= -hitSlopRect.left && + x < view.width + hitSlopRect.right && + y >= -hitSlopRect.top && + y < view.height + hitSlopRect.bottom) { + return true + } + return false + } else { + if (x >= 0 && x < view.width && y >= 0 && y < view.height) { + return true + } + return false + } + } + + private fun isTouchPointInViewWithOverflowInset(x: Float, y: Float, view: View): Boolean { + if (view !is ReactOverflowViewWithInset) { + return false + } + + val overflowInset = view.overflowInset + return (x >= overflowInset.left && x < view.width - overflowInset.right) && + (y >= overflowInset.top && y < view.height - 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 + * ViewGroup#isTransformedTouchPointInView()} + */ + private fun getChildPoint( + x: Float, + y: Float, + parent: ViewGroup, + child: View, + outLocalPoint: PointF + ) { + var localX = x + parent.scrollX - child.left + var localY = y + parent.scrollY - child.top + val matrix = child.matrix + if (!matrix.isIdentity) { + val localXY = matrixTransformCoords + localXY[0] = localX + localXY[1] = localY + val inverseMatrix = inverseMatrix + matrix.invert(inverseMatrix) + inverseMatrix.mapPoints(localXY) + localX = localXY[0] + localY = localXY[1] + } + outLocalPoint.set(localX, localY) + } + + /** + * 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 fun findTouchTargetViewWithPointerEvents( + eventCoords: FloatArray, + view: View, + pathAccumulator: MutableList? = null + ): View? { + var pointerEvents = + if (view is ReactPointerEventsView) { + view.pointerEvents + } else { + 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) { + pointerEvents = + when (pointerEvents) { + PointerEvents.AUTO -> PointerEvents.BOX_NONE + PointerEvents.BOX_ONLY -> PointerEvents.NONE + else -> pointerEvents + } + } + + return when (pointerEvents) { + PointerEvents.NONE -> { + // This view and its children can't be the target + null + } + PointerEvents.BOX_ONLY -> { + // This view may be the target, its children don't matter + val targetView = + findTouchTargetView( + eventCoords, view, EnumSet.of(TouchTargetReturnType.SELF), pathAccumulator) + targetView?.let { pathAccumulator?.add(ViewTarget(view.id, view)) } + targetView + } + PointerEvents.BOX_NONE -> { + // This view can't be the target, but its children might. + val targetView = + findTouchTargetView( + eventCoords, view, EnumSet.of(TouchTargetReturnType.CHILD), pathAccumulator) + + if (targetView != null) { + pathAccumulator?.add(ViewTarget(view.id, 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 is ReactCompoundView && isTouchPointInView(eventCoords[0], eventCoords[1], view)) { + val reactTag = + (view as ReactCompoundView).reactTagForTouch(eventCoords[0], eventCoords[1]) + // make sure we exclude the View itself because of the PointerEvents.BOX_NONE + if (reactTag != view.id) { + pathAccumulator?.add(ViewTarget(view.id, view)) + return view + } + } + + null + } + else -> { + if (pointerEvents != PointerEvents.AUTO) { + FLog.w(ReactConstants.TAG, "Unknown pointer event type: $pointerEvents") + } + // Either this view or one of its children is the target + if (view is ReactCompoundViewGroup && + isTouchPointInView(eventCoords[0], eventCoords[1], view) && + view.interceptsTouchEvent(eventCoords[0], eventCoords[1])) { + pathAccumulator?.add(ViewTarget(view.id, view)) + return view + } + + val result = + findTouchTargetView( + eventCoords, + view, + EnumSet.of(TouchTargetReturnType.SELF, TouchTargetReturnType.CHILD), + pathAccumulator) + result?.let { pathAccumulator?.add(ViewTarget(view.id, view)) } + result + } + } + } + + private fun getTouchTargetForView(targetView: View, viewX: Float, viewY: Float): Int = + if (targetView is ReactCompoundView) { + // Use coordinates relative to the view, which have been already computed by + // [findTouchTargetView]. + targetView.reactTagForTouch(viewX, viewY) + } else { + targetView.id + } + + // ViewTarget class as a nested class in TouchTargetHelper + public class ViewTarget internal constructor(private val viewId: Int, private val view: View?) { + + public fun getViewId(): Int = viewId + + public fun getView(): View? = view + + public override fun equals(other: Any?): Boolean { + // If the object is compared with itself then return true + if (this === other) { + return true + } + + // Check if other is an instance of ViewTarget. + if (other !is ViewTarget) { + return false + } + + // We only need to compare view id, as we assume the same view id will always map to the + // same view. + return other.getViewId() == viewId + } + + public override fun hashCode(): Int = viewId.hashCode() + } +}