From fe405b50f23fdcbcfbc924c945908287f5fb352f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 26 Oct 2026 13:31:32 +0100 Subject: [PATCH 1/2] [iOS] Report zero Pan velocity when the pointer rested before release --- .../apple/Handlers/RNPanHandler.m | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m index e67d02cf8c..0d5192946e 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m @@ -37,11 +37,18 @@ @interface RNBetterPanGestureRecognizer : UIPanGestureRecognizer #endif - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler; +- (CGPoint)reportedVelocityInView:(RNGHUIView *)view; @end +// A pointer that sent no move for this long is treated as stopped, like in +// Android's VelocityTracker and the web tracker. +static const NSTimeInterval pointerStoppedInterval = 0.04; + @implementation RNBetterPanGestureRecognizer { __weak RNGestureHandler *_gestureHandler; + NSTimeInterval _lastMoveTimestamp; + BOOL _restedBeforeRelease; #if TARGET_OS_OSX BOOL _blockAutomaticActivation; #else @@ -135,6 +142,9 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event [_gestureHandler reset]; } + _lastMoveTimestamp = event.timestamp; + _restedBeforeRelease = NO; + #if !TARGET_OS_TV && !TARGET_OS_OSX if (_hasCustomActivationCriteria) { // We use "minimumNumberOfTouches" property to prevent pan handler from recognizing @@ -169,6 +179,7 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event - (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event { + _lastMoveTimestamp = event.timestamp; [_gestureHandler.pointerTracker touchesMoved:touches withEvent:event]; if (self.state == UIGestureRecognizerStatePossible && [self shouldFailUnderCustomCriteria]) { @@ -212,6 +223,13 @@ - (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event #endif } +// Velocity for the event payload: zero on release when the pointer rested +// before lifting, the recognizer's estimate otherwise. +- (CGPoint)reportedVelocityInView:(RNGHUIView *)view +{ + return self.state == UIGestureRecognizerStateEnded && _restedBeforeRelease ? CGPointZero : [self velocityInView:view]; +} + - (void)interactionsEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; @@ -264,6 +282,8 @@ - (void)mouseDragged:(NSEvent *)event - (void)mouseUp:(NSEvent *)event { + _restedBeforeRelease = event.timestamp - _lastMoveTimestamp > pointerStoppedInterval; + [super mouseUp:event]; [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } @@ -286,6 +306,8 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + _restedBeforeRelease = event.timestamp - _lastMoveTimestamp > pointerStoppedInterval; + [super touchesEnded:touches withEvent:event]; [self interactionsEnded:touches withEvent:event]; } @@ -306,6 +328,7 @@ - (void)reset #if TARGET_OS_OSX _blockAutomaticActivation = NO; #endif + _restedBeforeRelease = NO; [super reset]; [_gestureHandler reset]; @@ -486,13 +509,16 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer #if TARGET_OS_OSX - (RNGestureHandlerEventExtraData *)eventExtraData:(NSPanGestureRecognizer *)recognizer { - 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] - withNumberOfTouches:1 - withPointerType:RNGestureHandlerMouse - withStylusData:nil]; + RNBetterPanGestureRecognizer *panRecognizer = (RNBetterPanGestureRecognizer *)recognizer; + + return + [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:self.coordinateView] + withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView] + withTranslation:[recognizer translationInView:recognizer.view.window.contentView] + withVelocity:[panRecognizer reportedVelocityInView:recognizer.view.window.contentView] + withNumberOfTouches:1 + withPointerType:RNGestureHandlerMouse + withStylusData:nil]; } #else - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)recognizer @@ -503,7 +529,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)rec forPan:[recognizer locationInView:self.coordinateView] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withTranslation:[recognizer translationInView:recognizer.view.window] - withVelocity:[recognizer velocityInView:recognizer.view.window] + withVelocity:[panRecognizer reportedVelocityInView:recognizer.view.window] withNumberOfTouches:recognizer.numberOfTouches withPointerType:_pointerType #if !TARGET_OS_TV From 5802e897dfe0ea1d79c73388dd03609b8ec377cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 26 Oct 2026 15:07:48 +0100 Subject: [PATCH 2/2] Do not restart the rest clock when an extra finger lands --- .../apple/Handlers/RNPanHandler.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m index 0d5192946e..cb0666b9e3 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m @@ -142,8 +142,10 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event [_gestureHandler reset]; } - _lastMoveTimestamp = event.timestamp; - _restedBeforeRelease = NO; + if (self.state == UIGestureRecognizerStatePossible) { + _lastMoveTimestamp = event.timestamp; + _restedBeforeRelease = NO; + } #if !TARGET_OS_TV && !TARGET_OS_OSX if (_hasCustomActivationCriteria) {