11package io .sentry .android .core .internal .gestures ;
22
33import android .content .res .Resources ;
4+ import android .graphics .Matrix ;
45import android .view .MotionEvent ;
56import android .view .View ;
67import android .view .ViewGroup ;
78import io .sentry .android .core .SentryAndroidOptions ;
89import io .sentry .internal .gestures .GestureTargetLocator ;
910import io .sentry .internal .gestures .UiElement ;
10- import java .util .LinkedList ;
11+ import java .util .ArrayDeque ;
1112import java .util .List ;
1213import java .util .Queue ;
1314import org .jetbrains .annotations .ApiStatus ;
1718@ ApiStatus .Internal
1819public final class ViewUtils {
1920
20- private static final int [] coordinates = new int [2 ];
21-
2221 /**
23- * Verifies if the given touch coordinates are within the bounds of the given view.
22+ * Verifies if the given touch coordinates, expressed in the view's own local coordinate space,
23+ * are within the bounds of the given view.
2424 *
2525 * @param view the view to check if the touch coordinates are within its bounds
26- * @param x - the x coordinate of a {@link MotionEvent}
27- * @param y - the y coordinate of {@link MotionEvent}
26+ * @param localX - the x coordinate of the touch, relative to the view's top-left corner
27+ * @param localY - the y coordinate of the touch, relative to the view's top-left corner
2828 * @return true if the touch coordinates are within the bounds of the view, false otherwise
2929 */
3030 private static boolean touchWithinBounds (
31- final @ Nullable View view , final float x , final float y ) {
31+ final @ Nullable View view , final float localX , final float localY ) {
3232 if (view == null ) {
3333 return false ;
3434 }
3535
36- view .getLocationOnScreen (coordinates );
37- int vx = coordinates [0 ];
38- int vy = coordinates [1 ];
36+ final int w = view .getWidth ();
37+ final int h = view .getHeight ();
3938
40- int w = view . getWidth ( );
41- int h = view . getHeight ();
39+ return !( localX < 0 || localX > w || localY < 0 || localY > h );
40+ }
4241
43- return !(x < vx || x > vx + w || y < vy || y > vy + h );
42+ /**
43+ * Maps a touch point expressed in the parent's local coordinate space into the child's local
44+ * coordinate space. This mirrors how {@link ViewGroup} dispatches touch events to its children
45+ * and lets us hit-test the whole tree with a single downward traversal, instead of calling {@link
46+ * View#getLocationOnScreen(int[])} (which walks up to the root) for every view.
47+ */
48+ private static @ NotNull ViewWithLocation mapToChild (
49+ final @ NotNull View child ,
50+ final float parentX ,
51+ final float parentY ,
52+ final int parentScrollX ,
53+ final int parentScrollY ) {
54+ float childX = parentX + parentScrollX - child .getLeft ();
55+ float childY = parentY + parentScrollY - child .getTop ();
56+
57+ final @ Nullable Matrix matrix = child .getMatrix ();
58+ if (matrix != null && !matrix .isIdentity ()) {
59+ final Matrix inverse = new Matrix ();
60+ if (matrix .invert (inverse )) {
61+ final float [] point = {childX , childY };
62+ inverse .mapPoints (point );
63+ childX = point [0 ];
64+ childY = point [1 ];
65+ }
66+ }
67+ return new ViewWithLocation (child , childX , childY );
4468 }
4569
4670 /**
4771 * Finds a target view, that has been selected/clicked by the given coordinates x and y and the
4872 * given {@code viewTargetSelector}.
4973 *
5074 * @param decorView - the root view of this window
51- * @param x - the x coordinate of a {@link MotionEvent}
52- * @param y - the y coordinate of {@link MotionEvent}
75+ * @param x - the x coordinate of a {@link MotionEvent}, relative to the decor view
76+ * @param y - the y coordinate of {@link MotionEvent}, relative to the decor view
5377 * @param targetType - the type of target to find
5478 * @return the {@link View} that contains the touch coordinates and complements the {@code
5579 * viewTargetSelector}
@@ -62,25 +86,35 @@ private static boolean touchWithinBounds(
6286 final UiElement .Type targetType ) {
6387
6488 final List <GestureTargetLocator > locators = options .getGestureTargetLocators ();
65- final Queue <View > queue = new LinkedList <>();
66- queue .add (decorView );
89+ final Queue <ViewWithLocation > queue = new ArrayDeque <>();
90+ // The touch coordinates from the MotionEvent are already relative to the decor view, i.e. in
91+ // its local coordinate space.
92+ queue .add (new ViewWithLocation (decorView , x , y ));
6793
6894 @ Nullable UiElement target = null ;
69- while (queue .size () > 0 ) {
70- final View view = queue .poll ();
95+ while (!queue .isEmpty ()) {
96+ final ViewWithLocation current = queue .poll ();
97+ final View view = current .view ;
7198
72- if (!touchWithinBounds (view , x , y )) {
99+ if (!touchWithinBounds (view , current . x , current . y )) {
73100 // if the touch is not hitting the view, skip traversal of its children
74101 continue ;
75102 }
76103
77104 if (view instanceof ViewGroup ) {
78105 final ViewGroup viewGroup = (ViewGroup ) view ;
106+ final int scrollX = viewGroup .getScrollX ();
107+ final int scrollY = viewGroup .getScrollY ();
79108 for (int i = 0 ; i < viewGroup .getChildCount (); i ++) {
80- queue .add (viewGroup .getChildAt (i ));
109+ final @ Nullable View child = viewGroup .getChildAt (i );
110+ if (child != null ) {
111+ queue .add (mapToChild (child , current .x , current .y , scrollX , scrollY ));
112+ }
81113 }
82114 }
83115
116+ // Locators receive the original decor-view-relative coordinates, as the Compose locator
117+ // hit-tests against window coordinates.
84118 for (int i = 0 ; i < locators .size (); i ++) {
85119 final GestureTargetLocator locator = locators .get (i );
86120 final @ Nullable UiElement newTarget = locator .locate (view , x , y , targetType );
@@ -96,6 +130,18 @@ private static boolean touchWithinBounds(
96130 return target ;
97131 }
98132
133+ private static final class ViewWithLocation {
134+ final @ NotNull View view ;
135+ final float x ;
136+ final float y ;
137+
138+ ViewWithLocation (final @ NotNull View view , final float x , final float y ) {
139+ this .view = view ;
140+ this .x = x ;
141+ this .y = y ;
142+ }
143+ }
144+
99145 /**
100146 * Retrieves the human-readable view id based on {@code view.getContext().getResources()}, falls
101147 * back to a hexadecimal id representation in case the view id is not available in the resources.
0 commit comments