diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/eventbuilders/NativeGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/eventbuilders/NativeGestureHandlerEventDataBuilder.kt index 8fee369abe..f4df732478 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/eventbuilders/NativeGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/eventbuilders/NativeGestureHandlerEventDataBuilder.kt @@ -1,15 +1,27 @@ package com.swmansion.gesturehandler.react.events.eventbuilders import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.PixelUtil import com.swmansion.gesturehandler.core.NativeViewGestureHandler class NativeGestureHandlerEventDataBuilder(handler: NativeViewGestureHandler) : GestureHandlerEventDataBuilder(handler) { private val pointerInside: Boolean = handler.isWithinBounds + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) eventData.putBoolean("pointerInside", pointerInside) + + with(eventData) { + putDouble("x", PixelUtil.toDIPFromPixel(x).toDouble()) + putDouble("y", PixelUtil.toDIPFromPixel(y).toDouble()) + putDouble("absoluteX", PixelUtil.toDIPFromPixel(absoluteX).toDouble()) + putDouble("absoluteY", PixelUtil.toDIPFromPixel(absoluteY).toDouble()) + } } } diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index d39c863ca1..85ad3e2e8e 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -197,14 +197,19 @@ - (void)unbindFromView [super unbindFromView]; } -- (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData +- (RNGestureHandlerEventExtraData *)extraDataForView:(UIView *)sender + event:(UIEvent *)event + pointerInside:(BOOL)pointerInside { - if (_lastActiveExtraData != nil && [_lastActiveExtraData.data isEqualToDictionary:extraData.data]) { - return YES; - } - - _lastActiveExtraData = extraData; - return NO; + RNGHUITouch *touch = [[event allTouches] anyObject]; + CGPoint position = touch ? [touch locationInView:sender] : CGPointZero; + CGPoint absolutePosition = touch ? [touch locationInView:nil] : [sender convertPoint:CGPointZero toView:nil]; + + return [RNGestureHandlerEventExtraData forPointerInside:pointerInside + withPosition:position + withAbsolutePosition:absolutePosition + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]; } - (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestureHandlerEventExtraData *)extraData @@ -218,21 +223,19 @@ - (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestu - (void)handleSwitch:(UIView *)sender { - [self sendEventsInState:RNGestureHandlerStateBegan - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:1 - withPointerType:_pointerType]]; - [self sendEventsInState:RNGestureHandlerStateActive - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:1 - withPointerType:_pointerType]]; - [self sendEventsInState:RNGestureHandlerStateEnd - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:1 - withPointerType:_pointerType]]; + CGPoint center = CGPointMake(CGRectGetMidX(sender.bounds), CGRectGetMidY(sender.bounds)); + CGPoint absoluteCenter = [sender convertPoint:center toView:nil]; + RNGestureHandlerEventExtraData * (^extraData)(void) = ^{ + return [RNGestureHandlerEventExtraData forPointerInside:YES + withPosition:center + withAbsolutePosition:absoluteCenter + withNumberOfTouches:1 + withPointerType:self->_pointerType]; + }; + + [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:sender.reactTag withExtraData:extraData()]; + [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData()]; + [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData()]; [self reset]; } @@ -262,9 +265,7 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + withExtraData:[self extraDataForView:sender event:event pointerInside:YES]]; } - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event @@ -275,16 +276,12 @@ - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + withExtraData:[self extraDataForView:sender event:event pointerInside:NO]]; } - (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event { - RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]; + RNGestureHandlerEventExtraData *extraData = [self extraDataForView:sender event:event pointerInside:YES]; [self sendActiveStateEventIfChangedForView:sender extraData:extraData]; [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData]; @@ -298,26 +295,20 @@ - (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event [control cancelTrackingWithEvent:event]; } else { [self sendActiveStateEventIfChangedForView:sender - extraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + extraData:[self extraDataForView:sender event:event pointerInside:NO]]; } } - (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event { [self sendActiveStateEventIfChangedForView:sender - extraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + extraData:[self extraDataForView:sender event:event pointerInside:YES]]; } - (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event { [self sendActiveStateEventIfChangedForView:sender - extraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + extraData:[self extraDataForView:sender event:event pointerInside:YES]]; } - (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event @@ -327,18 +318,14 @@ - (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event } [self sendActiveStateEventIfChangedForView:sender - extraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + extraData:[self extraDataForView:sender event:event pointerInside:NO]]; } - (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event { [self sendEventsInState:RNGestureHandlerStateCancelled forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + withExtraData:[self extraDataForView:sender event:event pointerInside:NO]]; } - (BOOL)wantsToAttachDirectlyToView @@ -346,17 +333,46 @@ - (BOOL)wantsToAttachDirectlyToView return YES; } +#endif + - (void)reset { [super reset]; _lastActiveExtraData = nil; } -#endif +- (BOOL)shouldSuppressActiveEvent:(RNGestureHandlerEventExtraData *)extraData +{ + NSDictionary *last = _lastActiveExtraData.data; + NSDictionary *current = extraData.data; + + if (last != nil && [last[@"pointerInside"] isEqual:current[@"pointerInside"]] && + [last[@"numberOfPointers"] isEqual:current[@"numberOfPointers"]] && + [last[@"pointerType"] isEqual:current[@"pointerType"]]) { + return YES; + } + + _lastActiveExtraData = extraData; + return NO; +} - (RNGestureHandlerEventExtraData *)eventExtraData:(RNDummyGestureRecognizer *)recognizer { +#if TARGET_OS_OSX + // macOS window coordinates are bottom-left origin; flip Y to match RN's + // top-left origin, mirroring RNGestureHandlerPointerTracker. + CGFloat windowHeight = recognizer.view.window.contentView.frame.size.height; + CGPoint yFlippedAbsolute = [recognizer locationInView:nil]; + CGPoint absolutePosition = CGPointMake(yFlippedAbsolute.x, windowHeight - yFlippedAbsolute.y); + CGPoint position = [recognizer.view convertPoint:absolutePosition fromView:recognizer.view.window.contentView]; +#else + CGPoint position = [recognizer locationInView:recognizer.view]; + CGPoint absolutePosition = [recognizer locationInView:nil]; +#endif + return [RNGestureHandlerEventExtraData forPointerInside:[self containsPointInView] + withPosition:position + withAbsolutePosition:absolutePosition withNumberOfTouches:1 withPointerType:RNGestureHandlerMouse]; } diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.h index 755bf76bfa..ab9a2e9319 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.h @@ -55,6 +55,8 @@ withNumberOfTouches:(NSUInteger)numberOfTouches withPointerType:(NSInteger)pointerType; + (RNGestureHandlerEventExtraData *)forPointerInside:(BOOL)pointerInside + withPosition:(CGPoint)position + withAbsolutePosition:(CGPoint)absolutePosition withNumberOfTouches:(NSUInteger)numberOfTouches withPointerType:(NSInteger)pointerType; @end diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.mm index 899cdbab9a..c548dbf748 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.mm @@ -157,11 +157,17 @@ + (RNGestureHandlerEventExtraData *)forEventType:(RNGHTouchEventType)eventType } + (RNGestureHandlerEventExtraData *)forPointerInside:(BOOL)pointerInside + withPosition:(CGPoint)position + withAbsolutePosition:(CGPoint)absolutePosition withNumberOfTouches:(NSUInteger)numberOfTouches withPointerType:(NSInteger)pointerType { return [[RNGestureHandlerEventExtraData alloc] initWithData:@{ @"pointerInside" : @(pointerInside), + @"x" : @(position.x), + @"y" : @(position.y), + @"absoluteX" : @(absolutePosition.x), + @"absoluteY" : @(absolutePosition.y), @"numberOfPointers" : @(numberOfTouches), @"pointerType" : @(pointerType) }]; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/NativeTypes.ts b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/NativeTypes.ts index fb3026e7b9..78e667dbb3 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/NativeTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/NativeTypes.ts @@ -40,6 +40,10 @@ export const NativeHandlerNativeProperties = new Set< export type NativeHandlerData = { pointerInside: boolean; + x: number; + y: number; + absoluteX: number; + absoluteY: number; }; export type NativeGestureProperties = diff --git a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts index 9f72b5e3f8..92738c14c8 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts @@ -2,7 +2,6 @@ import { Platform } from 'react-native'; import { type ActionType } from '../../ActionType'; import { State } from '../../State'; -import { deepEqual } from '../../utils'; import type { NativeHandlerData } from '../../v3/hooks/gestures/native/NativeTypes'; import type { HandlerData } from '../../v3/types'; import { SingleGestureName } from '../../v3/types'; @@ -284,20 +283,33 @@ export default class NativeViewGestureHandler extends GestureHandler { } protected override transformNativeEvent(): Record { + const absolute = this.tracker.getAbsoluteCoordsAverage(); + const relative = this.tracker.getRelativeCoordsAverage(); return { - pointerInside: this.delegate.isPointerInBounds( - this.tracker.getAbsoluteCoordsAverage() - ), + pointerInside: this.delegate.isPointerInBounds(absolute), + x: relative.x, + y: relative.y, + absoluteX: absolute.x, + absoluteY: absolute.y, }; } + private arePointerStatesEqual( + a: HandlerData, + b: HandlerData + ): boolean { + return ( + a.pointerInside === b.pointerInside && + a.numberOfPointers === b.numberOfPointers && + a.pointerType === b.pointerType + ); + } + protected override shouldSuppressActiveUpdate( handlerData: HandlerData ): boolean { - if ( - this.lastActiveHandlerData && - deepEqual(this.lastActiveHandlerData, handlerData) - ) { + const last = this.lastActiveHandlerData; + if (last && this.arePointerStatesEqual(last, handlerData)) { return true; } this.lastActiveHandlerData = handlerData;