17
17
import com .facebook .react .bridge .JSApplicationIllegalArgumentException ;
18
18
import com .facebook .react .bridge .UiThreadUtil ;
19
19
import com .facebook .react .touch .ReactHitSlopView ;
20
+ import java .util .EnumSet ;
20
21
21
22
/**
22
23
* Class responsible for identifying which react view should handle a given {@link MotionEvent}. It
@@ -80,7 +81,7 @@ public static int findTargetTagAndCoordinatesForTouch(
80
81
// Store eventCoords in array so that they are modified to be relative to the targetView found.
81
82
viewCoords [0 ] = eventX ;
82
83
viewCoords [1 ] = eventY ;
83
- View nativeTargetView = findTouchTargetView (viewCoords , viewGroup );
84
+ View nativeTargetView = findTouchTargetViewWithPointerEvents (viewCoords , viewGroup );
84
85
if (nativeTargetView != null ) {
85
86
View reactTargetView = findClosestReactAncestor (nativeTargetView );
86
87
if (reactTargetView != null ) {
@@ -100,6 +101,14 @@ private static View findClosestReactAncestor(View view) {
100
101
return view ;
101
102
}
102
103
104
+ /** Types of allowed return values from {@link #findTouchTargetView}. */
105
+ private enum TouchTargetReturnType {
106
+ /** Allow returning the view passed in through the parameters. */
107
+ SELF ,
108
+ /** Allow returning children of the view passed in through parameters. */
109
+ CHILD ,
110
+ }
111
+
103
112
/**
104
113
* Returns the touch target View that is either viewGroup or one if its descendants. This is a
105
114
* recursive DFS since view the entire tree must be parsed until the target is found. If the
@@ -111,43 +120,88 @@ private static View findClosestReactAncestor(View view) {
111
120
* be relative to the current viewGroup. When the method returns, it will contain the eventCoords
112
121
* relative to the targetView found.
113
122
*/
114
- private static View findTouchTargetView (float [] eventCoords , ViewGroup viewGroup ) {
115
- int childrenCount = viewGroup .getChildCount ();
116
- // Consider z-index when determining the touch target.
117
- ReactZIndexedViewGroup zIndexedViewGroup =
118
- viewGroup instanceof ReactZIndexedViewGroup ? (ReactZIndexedViewGroup ) viewGroup : null ;
119
- for (int i = childrenCount - 1 ; i >= 0 ; i --) {
120
- int childIndex =
121
- zIndexedViewGroup != null ? zIndexedViewGroup .getZIndexMappedChildIndex (i ) : i ;
122
- View child = viewGroup .getChildAt (childIndex );
123
- PointF childPoint = mTempPoint ;
124
- if (isTransformedTouchPointInView (
125
- eventCoords [0 ], eventCoords [1 ], viewGroup , child , childPoint )) {
126
- // If it is contained within the child View, the childPoint value will contain the view
127
- // coordinates relative to the child
123
+ private static View findTouchTargetView (
124
+ float [] eventCoords , View view , EnumSet <TouchTargetReturnType > allowReturnTouchTargetTypes ) {
125
+ // We prefer returning a child, so we check for a child that can handle the touch first
126
+ if (allowReturnTouchTargetTypes .contains (TouchTargetReturnType .CHILD )
127
+ && view instanceof ViewGroup ) {
128
+ ViewGroup viewGroup = (ViewGroup ) view ;
129
+ int childrenCount = viewGroup .getChildCount ();
130
+ // Consider z-index when determining the touch target.
131
+ ReactZIndexedViewGroup zIndexedViewGroup =
132
+ viewGroup instanceof ReactZIndexedViewGroup ? (ReactZIndexedViewGroup ) viewGroup : null ;
133
+ for (int i = childrenCount - 1 ; i >= 0 ; i --) {
134
+ int childIndex =
135
+ zIndexedViewGroup != null ? zIndexedViewGroup .getZIndexMappedChildIndex (i ) : i ;
136
+ View child = viewGroup .getChildAt (childIndex );
137
+ PointF childPoint = mTempPoint ;
138
+ getChildPoint (eventCoords [0 ], eventCoords [1 ], viewGroup , child , childPoint );
139
+ // The childPoint value will contain the view coordinates relative to the child.
128
140
// We need to store the existing X,Y for the viewGroup away as it is possible this child
129
141
// will not actually be the target and so we restore them if not
130
142
float restoreX = eventCoords [0 ];
131
143
float restoreY = eventCoords [1 ];
132
144
eventCoords [0 ] = childPoint .x ;
133
145
eventCoords [1 ] = childPoint .y ;
134
146
View targetView = findTouchTargetViewWithPointerEvents (eventCoords , child );
147
+
135
148
if (targetView != null ) {
136
- return targetView ;
149
+ // We don't allow touches on views that are outside the bounds of an `overflow: hidden`
150
+ // View
151
+ boolean inOverflowBounds = true ;
152
+ if (viewGroup instanceof ReactOverflowView ) {
153
+ @ Nullable String overflow = ((ReactOverflowView ) viewGroup ).getOverflow ();
154
+ if ((ViewProps .HIDDEN .equals (overflow ) || ViewProps .SCROLL .equals (overflow ))
155
+ && !isTouchPointInView (restoreX , restoreY , view )) {
156
+ inOverflowBounds = false ;
157
+ }
158
+ }
159
+ if (inOverflowBounds ) {
160
+ return targetView ;
161
+ }
137
162
}
138
163
eventCoords [0 ] = restoreX ;
139
164
eventCoords [1 ] = restoreY ;
140
165
}
141
166
}
142
- return viewGroup ;
167
+
168
+ // Check if parent can handle the touch after the children
169
+ if (allowReturnTouchTargetTypes .contains (TouchTargetReturnType .SELF )
170
+ && isTouchPointInView (eventCoords [0 ], eventCoords [1 ], view )) {
171
+ return view ;
172
+ }
173
+
174
+ return null ;
143
175
}
144
176
145
177
/**
146
- * Returns whether the touch point is within the child View It is transform aware and will invert
147
- * the transform Matrix to find the true local points This code is taken from {@link
178
+ * Checks whether a touch at {@code x} and {@code y} are within the bounds of the View. Both
179
+ * {@code x} and {@code y} must be relative to the top-left corner of the view.
180
+ */
181
+ private static boolean isTouchPointInView (float x , float y , View view ) {
182
+ if (view instanceof ReactHitSlopView && ((ReactHitSlopView ) view ).getHitSlopRect () != null ) {
183
+ Rect hitSlopRect = ((ReactHitSlopView ) view ).getHitSlopRect ();
184
+ if ((x >= -hitSlopRect .left && x < (view .getWidth ()) + hitSlopRect .right )
185
+ && (y >= -hitSlopRect .top && y < (view .getHeight ()) + hitSlopRect .bottom )) {
186
+ return true ;
187
+ }
188
+
189
+ return false ;
190
+ } else {
191
+ if ((x >= 0 && x < (view .getWidth ())) && (y >= 0 && y < (view .getHeight ()))) {
192
+ return true ;
193
+ }
194
+
195
+ return false ;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Returns the coordinates of a touch in the child View. It is transform aware and will invert the
201
+ * transform Matrix to find the true local points This code is taken from {@link
148
202
* ViewGroup#isTransformedTouchPointInView()}
149
203
*/
150
- private static boolean isTransformedTouchPointInView (
204
+ private static void getChildPoint (
151
205
float x , float y , ViewGroup parent , View child , PointF outLocalPoint ) {
152
206
float localX = x + parent .getScrollX () - child .getLeft ();
153
207
float localY = y + parent .getScrollY () - child .getTop ();
@@ -162,26 +216,7 @@ private static boolean isTransformedTouchPointInView(
162
216
localX = localXY [0 ];
163
217
localY = localXY [1 ];
164
218
}
165
- if (child instanceof ReactHitSlopView && ((ReactHitSlopView ) child ).getHitSlopRect () != null ) {
166
- Rect hitSlopRect = ((ReactHitSlopView ) child ).getHitSlopRect ();
167
- if ((localX >= -hitSlopRect .left
168
- && localX < (child .getRight () - child .getLeft ()) + hitSlopRect .right )
169
- && (localY >= -hitSlopRect .top
170
- && localY < (child .getBottom () - child .getTop ()) + hitSlopRect .bottom )) {
171
- outLocalPoint .set (localX , localY );
172
- return true ;
173
- }
174
-
175
- return false ;
176
- } else {
177
- if ((localX >= 0 && localX < (child .getRight () - child .getLeft ()))
178
- && (localY >= 0 && localY < (child .getBottom () - child .getTop ()))) {
179
- outLocalPoint .set (localX , localY );
180
- return true ;
181
- }
182
-
183
- return false ;
184
- }
219
+ outLocalPoint .set (localX , localY );
185
220
}
186
221
187
222
/**
@@ -211,45 +246,43 @@ private static boolean isTransformedTouchPointInView(
211
246
return null ;
212
247
213
248
} else if (pointerEvents == PointerEvents .BOX_ONLY ) {
214
- // This view is the target, its children don't matter
215
- return view ;
249
+ // This view may be the target, its children don't matter
250
+ return findTouchTargetView ( eventCoords , view , EnumSet . of ( TouchTargetReturnType . SELF )) ;
216
251
217
252
} else if (pointerEvents == PointerEvents .BOX_NONE ) {
218
253
// This view can't be the target, but its children might.
219
- if ( view instanceof ViewGroup ) {
220
- View targetView = findTouchTargetView (eventCoords , ( ViewGroup ) view );
221
- if (targetView != view ) {
222
- return targetView ;
223
- }
254
+ View targetView =
255
+ findTouchTargetView (eventCoords , view , EnumSet . of ( TouchTargetReturnType . CHILD ) );
256
+ if (targetView != null ) {
257
+ return targetView ;
258
+ }
224
259
225
- // PointerEvents.BOX_NONE means that this react element cannot receive pointer events.
226
- // However, there might be virtual children that can receive pointer events, in which case
227
- // we still want to return this View and dispatch a pointer event to the virtual element.
228
- // Note that this currently only applies to Nodes/FlatViewGroup as it's the only class that
229
- // is both a ViewGroup and ReactCompoundView (ReactTextView is a ReactCompoundView but not a
230
- // ViewGroup).
231
- if (view instanceof ReactCompoundView ) {
232
- int reactTag =
233
- ((ReactCompoundView ) view ).reactTagForTouch (eventCoords [0 ], eventCoords [1 ]);
234
- if (reactTag != view .getId ()) {
235
- // make sure we exclude the View itself because of the PointerEvents.BOX_NONE
236
- return view ;
237
- }
260
+ // PointerEvents.BOX_NONE means that this react element cannot receive pointer events.
261
+ // However, there might be virtual children that can receive pointer events, in which case
262
+ // we still want to return this View and dispatch a pointer event to the virtual element.
263
+ // Note that this currently only applies to Nodes/FlatViewGroup as it's the only class that
264
+ // is both a ViewGroup and ReactCompoundView (ReactTextView is a ReactCompoundView but not a
265
+ // ViewGroup).
266
+ if (view instanceof ReactCompoundView
267
+ && isTouchPointInView (eventCoords [0 ], eventCoords [1 ], view )) {
268
+ int reactTag = ((ReactCompoundView ) view ).reactTagForTouch (eventCoords [0 ], eventCoords [1 ]);
269
+ if (reactTag != view .getId ()) {
270
+ // make sure we exclude the View itself because of the PointerEvents.BOX_NONE
271
+ return view ;
238
272
}
239
273
}
274
+
240
275
return null ;
241
276
242
277
} else if (pointerEvents == PointerEvents .AUTO ) {
243
278
// Either this view or one of its children is the target
244
- if (view instanceof ReactCompoundViewGroup ) {
245
- if ((( ReactCompoundViewGroup ) view ). interceptsTouchEvent (eventCoords [0 ], eventCoords [1 ])) {
246
- return view ;
247
- }
279
+ if (view instanceof ReactCompoundViewGroup
280
+ && isTouchPointInView (eventCoords [0 ], eventCoords [1 ], view )
281
+ && (( ReactCompoundViewGroup ) view ). interceptsTouchEvent ( eventCoords [ 0 ], eventCoords [ 1 ])) {
282
+ return view ;
248
283
}
249
- if (view instanceof ViewGroup ) {
250
- return findTouchTargetView (eventCoords , (ViewGroup ) view );
251
- }
252
- return view ;
284
+ return findTouchTargetView (
285
+ eventCoords , view , EnumSet .of (TouchTargetReturnType .SELF , TouchTargetReturnType .CHILD ));
253
286
254
287
} else {
255
288
throw new JSApplicationIllegalArgumentException (
0 commit comments