Skip to content

Commit f1de825

Browse files
authored
[Native] Use correct coordinate space when using native detector (#4137)
## Description Currently, all native handlers are using the coordinate space of the view they are attached to for calculating events. If the handler is attached directly to the view (v1, v2, intercepting detector), this is correct, but when the handler is attached to a wrapper (native detector), the coordinates may drift if the child is transformed. This PR addresses this by using the child of the native detector as the coordinate space base. ## Test plan Tested on #3124
1 parent c415c99 commit f1de825

10 files changed

Lines changed: 64 additions & 52 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import android.view.MotionEvent
99
import android.view.MotionEvent.PointerCoords
1010
import android.view.MotionEvent.PointerProperties
1111
import android.view.View
12-
import androidx.core.view.isNotEmpty
1312
import com.facebook.react.bridge.Arguments
1413
import com.facebook.react.bridge.ReactContext
1514
import com.facebook.react.bridge.ReadableMap
@@ -48,6 +47,26 @@ open class GestureHandler {
4847
}
4948
}
5049

50+
/**
51+
* The view whose coordinate space should be used when reporting event positions to JS.
52+
*
53+
* Handlers attached via the V3 NativeDetector are registered against the DetectorView wrapper,
54+
* which never carries user-applied transforms — those live on its child. When the detector has
55+
* exactly one child we descend into it so reported coordinates match the visible (transformed)
56+
* view, the same coordinate space V2 and the V3 VirtualGestureDetector report in. With
57+
* multiple children there is no JS-side way to disambiguate which child caught the pointer,
58+
* so we keep the detector itself as the reference frame.
59+
*/
60+
val coordinateView: View?
61+
get() {
62+
val v = view
63+
return if (v is RNGestureHandlerDetectorView && v.childCount == 1) {
64+
v.getChildAt(0)
65+
} else {
66+
v
67+
}
68+
}
69+
5170
var state = STATE_UNDETERMINED
5271
private set
5372
var x = 0f
@@ -387,42 +406,13 @@ open class GestureHandler {
387406

388407
numberOfPointers = adaptedTransformedEvent.pointerCount
389408

390-
// TODO: this is likely wrong, and the transformed event itself should be
391-
// in the coordinate system of the child view, but I'm not sure of the
392-
// consequences
393-
val detectorView = hostDetectorView
394-
if (detectorView != null && view == detectorView && detectorView.isNotEmpty()) {
395-
val outPoint = PointF()
396-
var foundChild = false
397-
398-
for (i in 0 until detectorView.childCount) {
399-
val child = detectorView.getChildAt(i)
400-
GestureHandlerOrchestrator.transformPointToChildViewCoords(
401-
adaptedTransformedEvent.x,
402-
adaptedTransformedEvent.y,
403-
detectorView,
404-
child,
405-
outPoint,
406-
)
407-
if (isWithinBounds(child, outPoint.x, outPoint.y)) {
408-
x = outPoint.x
409-
y = outPoint.y
410-
isWithinBounds = true
411-
foundChild = true
412-
break
413-
}
414-
}
415-
416-
if (!foundChild) {
417-
x = adaptedTransformedEvent.x
418-
y = adaptedTransformedEvent.y
419-
isWithinBounds = false
420-
}
421-
} else {
422-
x = adaptedTransformedEvent.x
423-
y = adaptedTransformedEvent.y
424-
isWithinBounds = isWithinBounds(view, x, y)
425-
}
409+
x = adaptedTransformedEvent.x
410+
y = adaptedTransformedEvent.y
411+
// The orchestrator transforms incoming events into the coordinate space of the detector's
412+
// child (when the handler is attached to a NativeDetector wrapper), so bounds-checking must
413+
// also use that child rather than the wrapper, otherwise hit-testing would ignore the user's
414+
// transforms applied to the visible view.
415+
isWithinBounds = isWithinBounds(coordinateView, x, y)
426416

427417
if (shouldCancelWhenOutside) {
428418
if (!isWithinBounds && (state == STATE_ACTIVE || state == STATE_BEGAN)) {
@@ -872,7 +862,7 @@ open class GestureHandler {
872862
* This method modifies and transforms the received point.
873863
*/
874864
protected fun transformPoint(point: PointF): PointF =
875-
orchestrator?.transformPointToViewCoords(this.view, point) ?: run {
865+
orchestrator?.transformPointToViewCoords(coordinateView, point) ?: run {
876866
point.x = Float.NaN
877867
point.y = Float.NaN
878868
point

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ class GestureHandlerOrchestrator(
297297
}
298298

299299
val action = sourceEvent.actionMasked
300-
val event = transformEventToViewCoords(handler.view, MotionEvent.obtain(sourceEvent))
300+
val event = transformEventToViewCoords(handler.coordinateView, MotionEvent.obtain(sourceEvent))
301301

302302
if (handler.needsPointerData) {
303303
handler.updatePointerData(event, sourceEvent)

packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ - (void)updateConfig:(NSDictionary *)config
170170
- (RNGestureHandlerEventExtraData *)eventExtraData:(RNForceTouchGestureRecognizer *)recognizer
171171
{
172172
return [RNGestureHandlerEventExtraData forForce:recognizer.force
173-
forPosition:[recognizer locationInView:recognizer.view]
173+
forPosition:[recognizer locationInView:self.coordinateView]
174174
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
175175
withNumberOfTouches:recognizer.numberOfTouches
176176
withPointerType:_pointerType];

packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ - (void)setCurrentPointerType:(RNGestureHandlerPointerType)pointerType
177177

178178
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer
179179
{
180-
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view]
180+
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView]
181181
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
182182
withPointerType:_pointerType];
183183
}

packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
287287

288288
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer
289289
{
290-
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view]
290+
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView]
291291
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
292292
withNumberOfTouches:recognizer.numberOfTouches
293293
withDuration:[(RNBetterLongPressGestureRecognizer *)recognizer getDuration]
@@ -298,7 +298,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recogn
298298

299299
- (RNGestureHandlerEventExtraData *)eventExtraData:(NSGestureRecognizer *)recognizer
300300
{
301-
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view]
301+
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView]
302302
withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView]
303303
withNumberOfTouches:1
304304
withDuration:[(RNBetterLongPressGestureRecognizer *)recognizer getDuration]

packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
452452
#if TARGET_OS_OSX
453453
- (RNGestureHandlerEventExtraData *)eventExtraData:(NSPanGestureRecognizer *)recognizer
454454
{
455-
return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:recognizer.view]
455+
return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:self.coordinateView]
456456
withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView]
457457
withTranslation:[recognizer translationInView:recognizer.view.window.contentView]
458458
withVelocity:[recognizer velocityInView:recognizer.view.window.contentView]
@@ -466,7 +466,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)rec
466466
RNBetterPanGestureRecognizer *panRecognizer = (RNBetterPanGestureRecognizer *)recognizer;
467467

468468
return [RNGestureHandlerEventExtraData
469-
forPan:[recognizer locationInView:recognizer.view]
469+
forPan:[recognizer locationInView:self.coordinateView]
470470
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
471471
withTranslation:[recognizer translationInView:recognizer.view.window]
472472
withVelocity:[recognizer velocityInView:recognizer.view.window]

packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ - (instancetype)initWithTag:(NSNumber *)tag
167167
- (RNGestureHandlerEventExtraData *)eventExtraData:(NSMagnificationGestureRecognizer *)recognizer
168168
{
169169
return [RNGestureHandlerEventExtraData forPinch:recognizer.magnification
170-
withFocalPoint:[recognizer locationInView:recognizer.view]
170+
withFocalPoint:[recognizer locationInView:self.coordinateView]
171171
withVelocity:((RNBetterPinchRecognizer *)recognizer).velocity
172172
withNumberOfTouches:2
173173
withPointerType:RNGestureHandlerMouse];
@@ -177,20 +177,21 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPinchGestureRecognizer *)r
177177
{
178178
CGPoint focalPoint;
179179
NSUInteger numberOfTouches = recognizer.numberOfTouches;
180+
RNGHUIView *coordinateView = self.coordinateView;
180181

181182
if (numberOfTouches > 0) {
182183
CGPoint accumulatedPoint = CGPointZero;
183184

184185
for (int i = 0; i < numberOfTouches; i++) {
185-
CGPoint location = [recognizer locationOfTouch:i inView:recognizer.view];
186+
CGPoint location = [recognizer locationOfTouch:i inView:coordinateView];
186187
accumulatedPoint.x += location.x;
187188
accumulatedPoint.y += location.y;
188189
}
189190

190191
focalPoint = CGPointMake(accumulatedPoint.x / numberOfTouches, accumulatedPoint.y / numberOfTouches);
191192
} else {
192193
// Trackpad pinch gestures may report 0 touches - use the recognizer's location instead
193-
focalPoint = [recognizer locationInView:recognizer.view];
194+
focalPoint = [recognizer locationInView:coordinateView];
194195
}
195196

196197
return [RNGestureHandlerEventExtraData forPinch:recognizer.scale

packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ - (instancetype)initWithTag:(NSNumber *)tag
161161
- (RNGestureHandlerEventExtraData *)eventExtraData:(NSRotationGestureRecognizer *)recognizer
162162
{
163163
return [RNGestureHandlerEventExtraData forRotation:-recognizer.rotation
164-
withAnchorPoint:[recognizer locationInView:recognizer.view]
164+
withAnchorPoint:[recognizer locationInView:self.coordinateView]
165165
withVelocity:((RNBetterRotationRecognizer *)recognizer).velocity
166166
withNumberOfTouches:2
167167
withPointerType:RNGestureHandlerMouse];
@@ -170,7 +170,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(NSRotationGestureRecognizer
170170
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIRotationGestureRecognizer *)recognizer
171171
{
172172
return [RNGestureHandlerEventExtraData forRotation:recognizer.rotation
173-
withAnchorPoint:[recognizer locationInView:recognizer.view]
173+
withAnchorPoint:[recognizer locationInView:self.coordinateView]
174174
withVelocity:recognizer.velocity
175175
withNumberOfTouches:recognizer.numberOfTouches
176176
withPointerType:_pointerType];

packages/react-native-gesture-handler/apple/RNGestureHandler.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@
8888
@property (nonatomic, copy, nullable) NSNumber *viewTag;
8989
@property (nonatomic, readonly) RNGestureHandlerState lastState;
9090

91+
/**
92+
The view whose coordinate space should be used when reporting event positions to JS.
93+
Handlers attached via the V3 NativeDetector are bound to the `RNGestureHandlerDetector` wrapper,
94+
which never carries user-applied transforms — those live on its child. When the detector has
95+
exactly one subview we descend into it so reported coordinates match the visible (transformed)
96+
view, the same coordinate space V2 and the V3 VirtualGestureDetector report in. With multiple
97+
subviews there is no JS-side way to disambiguate which child caught the pointer, so we keep
98+
the detector itself as the reference frame.
99+
*/
100+
@property (nonatomic, readonly, nullable) RNGHUIView *coordinateView;
101+
91102
- (BOOL)isViewParagraphComponent:(nullable RNGHUIView *)view;
92103
- (nonnull RNGHUIView *)chooseViewForInteraction:(nonnull UIGestureRecognizer *)recognizer;
93104
- (void)bindToView:(nonnull RNGHUIView *)view;

packages/react-native-gesture-handler/apple/RNGestureHandler.mm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,12 @@ - (void)unbindFromView
286286
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer
287287
{
288288
#if TARGET_OS_OSX
289-
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view]
289+
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView]
290290
withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView]
291291
withNumberOfTouches:1
292292
withPointerType:RNGestureHandlerMouse];
293293
#else
294-
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view]
294+
return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView]
295295
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
296296
withNumberOfTouches:recognizer.numberOfTouches
297297
withPointerType:_pointerType];
@@ -307,6 +307,16 @@ - (RNGHUIView *)chooseViewForInteraction:(UIGestureRecognizer *)recognizer
307307
return [self isViewParagraphComponent:recognizer.view] ? recognizer.view.subviews[0] : recognizer.view;
308308
}
309309

310+
- (RNGHUIView *)coordinateView
311+
{
312+
RNGHUIView *recognizerView = _recognizer.view;
313+
if ([self usesNativeOrVirtualDetector] && recognizerView == self.hostDetectorView &&
314+
recognizerView.subviews.count == 1) {
315+
return recognizerView.subviews[0];
316+
}
317+
return recognizerView;
318+
}
319+
310320
- (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData
311321
{
312322
return NO;

0 commit comments

Comments
 (0)