diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 2042529e2d..88c85744d1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -9,7 +9,6 @@ import android.view.MotionEvent import android.view.MotionEvent.PointerCoords import android.view.MotionEvent.PointerProperties import android.view.View -import androidx.core.view.isNotEmpty import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableMap @@ -48,6 +47,26 @@ open class GestureHandler { } } + /** + * The view whose coordinate space should be used when reporting event positions to JS. + * + * Handlers attached via the V3 NativeDetector are registered against the DetectorView wrapper, + * which never carries user-applied transforms — those live on its child. When the detector has + * exactly one child we descend into it so reported coordinates match the visible (transformed) + * view, the same coordinate space V2 and the V3 VirtualGestureDetector report in. With + * multiple children there is no JS-side way to disambiguate which child caught the pointer, + * so we keep the detector itself as the reference frame. + */ + val coordinateView: View? + get() { + val v = view + return if (v is RNGestureHandlerDetectorView && v.childCount == 1) { + v.getChildAt(0) + } else { + v + } + } + var state = STATE_UNDETERMINED private set var x = 0f @@ -387,42 +406,13 @@ open class GestureHandler { numberOfPointers = adaptedTransformedEvent.pointerCount - // TODO: this is likely wrong, and the transformed event itself should be - // in the coordinate system of the child view, but I'm not sure of the - // consequences - val detectorView = hostDetectorView - if (detectorView != null && view == detectorView && detectorView.isNotEmpty()) { - val outPoint = PointF() - var foundChild = false - - for (i in 0 until detectorView.childCount) { - val child = detectorView.getChildAt(i) - GestureHandlerOrchestrator.transformPointToChildViewCoords( - adaptedTransformedEvent.x, - adaptedTransformedEvent.y, - detectorView, - child, - outPoint, - ) - if (isWithinBounds(child, outPoint.x, outPoint.y)) { - x = outPoint.x - y = outPoint.y - isWithinBounds = true - foundChild = true - break - } - } - - if (!foundChild) { - x = adaptedTransformedEvent.x - y = adaptedTransformedEvent.y - isWithinBounds = false - } - } else { - x = adaptedTransformedEvent.x - y = adaptedTransformedEvent.y - isWithinBounds = isWithinBounds(view, x, y) - } + x = adaptedTransformedEvent.x + y = adaptedTransformedEvent.y + // The orchestrator transforms incoming events into the coordinate space of the detector's + // child (when the handler is attached to a NativeDetector wrapper), so bounds-checking must + // also use that child rather than the wrapper, otherwise hit-testing would ignore the user's + // transforms applied to the visible view. + isWithinBounds = isWithinBounds(coordinateView, x, y) if (shouldCancelWhenOutside) { if (!isWithinBounds && (state == STATE_ACTIVE || state == STATE_BEGAN)) { @@ -872,7 +862,7 @@ open class GestureHandler { * This method modifies and transforms the received point. */ protected fun transformPoint(point: PointF): PointF = - orchestrator?.transformPointToViewCoords(this.view, point) ?: run { + orchestrator?.transformPointToViewCoords(coordinateView, point) ?: run { point.x = Float.NaN point.y = Float.NaN point diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 21fe9d94fb..f63916e7f7 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -297,7 +297,7 @@ class GestureHandlerOrchestrator( } val action = sourceEvent.actionMasked - val event = transformEventToViewCoords(handler.view, MotionEvent.obtain(sourceEvent)) + val event = transformEventToViewCoords(handler.coordinateView, MotionEvent.obtain(sourceEvent)) if (handler.needsPointerData) { handler.updatePointerData(event, sourceEvent) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m index 62a792a2c9..113dcddafd 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNForceTouchHandler.m @@ -170,7 +170,7 @@ - (void)updateConfig:(NSDictionary *)config - (RNGestureHandlerEventExtraData *)eventExtraData:(RNForceTouchGestureRecognizer *)recognizer { return [RNGestureHandlerEventExtraData forForce:recognizer.force - forPosition:[recognizer locationInView:recognizer.view] + forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withNumberOfTouches:recognizer.numberOfTouches withPointerType:_pointerType]; diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m index e854671b84..3cd9a115e9 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNHoverHandler.m @@ -177,7 +177,7 @@ - (void)setCurrentPointerType:(RNGestureHandlerPointerType)pointerType - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer { - return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withPointerType:_pointerType]; } diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m index ae8ec2953e..00cdf0421e 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m @@ -287,7 +287,7 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer { - return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withNumberOfTouches:recognizer.numberOfTouches withDuration:[(RNBetterLongPressGestureRecognizer *)recognizer getDuration] @@ -298,7 +298,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recogn - (RNGestureHandlerEventExtraData *)eventExtraData:(NSGestureRecognizer *)recognizer { - return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView] withNumberOfTouches:1 withDuration:[(RNBetterLongPressGestureRecognizer *)recognizer getDuration] diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m index 4396f8543b..3d99add8ad 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m @@ -452,7 +452,7 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer #if TARGET_OS_OSX - (RNGestureHandlerEventExtraData *)eventExtraData:(NSPanGestureRecognizer *)recognizer { - return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView] withTranslation:[recognizer translationInView:recognizer.view.window.contentView] withVelocity:[recognizer velocityInView:recognizer.view.window.contentView] @@ -466,7 +466,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)rec RNBetterPanGestureRecognizer *panRecognizer = (RNBetterPanGestureRecognizer *)recognizer; return [RNGestureHandlerEventExtraData - forPan:[recognizer locationInView:recognizer.view] + forPan:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withTranslation:[recognizer translationInView:recognizer.view.window] withVelocity:[recognizer velocityInView:recognizer.view.window] diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m index 96de2d9830..7a2b224f29 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m @@ -167,7 +167,7 @@ - (instancetype)initWithTag:(NSNumber *)tag - (RNGestureHandlerEventExtraData *)eventExtraData:(NSMagnificationGestureRecognizer *)recognizer { return [RNGestureHandlerEventExtraData forPinch:recognizer.magnification - withFocalPoint:[recognizer locationInView:recognizer.view] + withFocalPoint:[recognizer locationInView:self.coordinateView] withVelocity:((RNBetterPinchRecognizer *)recognizer).velocity withNumberOfTouches:2 withPointerType:RNGestureHandlerMouse]; @@ -177,12 +177,13 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPinchGestureRecognizer *)r { CGPoint focalPoint; NSUInteger numberOfTouches = recognizer.numberOfTouches; + RNGHUIView *coordinateView = self.coordinateView; if (numberOfTouches > 0) { CGPoint accumulatedPoint = CGPointZero; for (int i = 0; i < numberOfTouches; i++) { - CGPoint location = [recognizer locationOfTouch:i inView:recognizer.view]; + CGPoint location = [recognizer locationOfTouch:i inView:coordinateView]; accumulatedPoint.x += location.x; accumulatedPoint.y += location.y; } @@ -190,7 +191,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPinchGestureRecognizer *)r focalPoint = CGPointMake(accumulatedPoint.x / numberOfTouches, accumulatedPoint.y / numberOfTouches); } else { // Trackpad pinch gestures may report 0 touches - use the recognizer's location instead - focalPoint = [recognizer locationInView:recognizer.view]; + focalPoint = [recognizer locationInView:coordinateView]; } return [RNGestureHandlerEventExtraData forPinch:recognizer.scale diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m index 85c2be9aca..86896909f0 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m @@ -161,7 +161,7 @@ - (instancetype)initWithTag:(NSNumber *)tag - (RNGestureHandlerEventExtraData *)eventExtraData:(NSRotationGestureRecognizer *)recognizer { return [RNGestureHandlerEventExtraData forRotation:-recognizer.rotation - withAnchorPoint:[recognizer locationInView:recognizer.view] + withAnchorPoint:[recognizer locationInView:self.coordinateView] withVelocity:((RNBetterRotationRecognizer *)recognizer).velocity withNumberOfTouches:2 withPointerType:RNGestureHandlerMouse]; @@ -170,7 +170,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(NSRotationGestureRecognizer - (RNGestureHandlerEventExtraData *)eventExtraData:(UIRotationGestureRecognizer *)recognizer { return [RNGestureHandlerEventExtraData forRotation:recognizer.rotation - withAnchorPoint:[recognizer locationInView:recognizer.view] + withAnchorPoint:[recognizer locationInView:self.coordinateView] withVelocity:recognizer.velocity withNumberOfTouches:recognizer.numberOfTouches withPointerType:_pointerType]; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 0b66cff5b3..49aa70ba7e 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -88,6 +88,17 @@ @property (nonatomic, copy, nullable) NSNumber *viewTag; @property (nonatomic, readonly) RNGestureHandlerState lastState; +/** + The view whose coordinate space should be used when reporting event positions to JS. + Handlers attached via the V3 NativeDetector are bound to the `RNGestureHandlerDetector` wrapper, + which never carries user-applied transforms — those live on its child. When the detector has + exactly one subview we descend into it so reported coordinates match the visible (transformed) + view, the same coordinate space V2 and the V3 VirtualGestureDetector report in. With multiple + subviews there is no JS-side way to disambiguate which child caught the pointer, so we keep + the detector itself as the reference frame. + */ +@property (nonatomic, readonly, nullable) RNGHUIView *coordinateView; + - (BOOL)isViewParagraphComponent:(nullable RNGHUIView *)view; - (nonnull RNGHUIView *)chooseViewForInteraction:(nonnull UIGestureRecognizer *)recognizer; - (void)bindToView:(nonnull RNGHUIView *)view; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 8cf4bf41f3..0a5452fc2d 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -286,12 +286,12 @@ - (void)unbindFromView - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer { #if TARGET_OS_OSX - return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView] withNumberOfTouches:1 withPointerType:RNGestureHandlerMouse]; #else - return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] + return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withNumberOfTouches:recognizer.numberOfTouches withPointerType:_pointerType]; @@ -307,6 +307,16 @@ - (RNGHUIView *)chooseViewForInteraction:(UIGestureRecognizer *)recognizer return [self isViewParagraphComponent:recognizer.view] ? recognizer.view.subviews[0] : recognizer.view; } +- (RNGHUIView *)coordinateView +{ + RNGHUIView *recognizerView = _recognizer.view; + if ([self usesNativeOrVirtualDetector] && recognizerView == self.hostDetectorView && + recognizerView.subviews.count == 1) { + return recognizerView.subviews[0]; + } + return recognizerView; +} + - (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData { return NO;