Skip to content

Commit b19e233

Browse files
authored
[iOS] Report zero Pan velocity when the pointer rested before release (#4642)
## Description Follow-up to the web fix for #4636, same rule on iOS and macOS. The Pan handler reports UIKit's `velocityInView:` in every event, including the end event. That estimate freezes at the last touch move, so a drag that stops, holds still, and then lifts reports the velocity of the drag in `onEnd`, and a `withDecay` started from it flings a pointer that had come to rest. Android reports 0 in that case, since its `VelocityTracker` treats a pointer as stopped after 40 ms without a sample. `RNBetterPanGestureRecognizer` now stamps the time of every move and, on release, records whether more than 40 ms passed since the last move. The check runs before the superclass handles the release, because that is where the gesture ends and the action is dispatched. The event payload reads the velocity through `reportedVelocityInView:`, which returns zero in the ended state after such a rest and UIKit's estimate otherwise. Update events, activation criteria and failure paths are unchanged, and so is a release within 40 ms of the last move. ## Test plan Tested on "Velocity Test" example - iPhone 17 simulator, v3 `usePanGesture` screen, a 160 px drag in 48 ms driven with argent: released at the last move `velocityX` 2517, released 19 ms later 2587, released 500 ms later 0 (2657 before the change). - macOS and tvOS share the change and compile the same paths, not run on a device.
1 parent 613be1c commit b19e233

1 file changed

Lines changed: 36 additions & 8 deletions

File tree

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,18 @@ @interface RNBetterPanGestureRecognizer : UIPanGestureRecognizer
3737
#endif
3838

3939
- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler;
40+
- (CGPoint)reportedVelocityInView:(RNGHUIView *)view;
4041

4142
@end
4243

44+
// A pointer that sent no move for this long is treated as stopped, like in
45+
// Android's VelocityTracker and the web tracker.
46+
static const NSTimeInterval pointerStoppedInterval = 0.04;
47+
4348
@implementation RNBetterPanGestureRecognizer {
4449
__weak RNGestureHandler *_gestureHandler;
50+
NSTimeInterval _lastMoveTimestamp;
51+
BOOL _restedBeforeRelease;
4552
#if TARGET_OS_OSX
4653
BOOL _blockAutomaticActivation;
4754
#else
@@ -135,6 +142,11 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event
135142
[_gestureHandler reset];
136143
}
137144

145+
if (self.state == UIGestureRecognizerStatePossible) {
146+
_lastMoveTimestamp = event.timestamp;
147+
_restedBeforeRelease = NO;
148+
}
149+
138150
#if !TARGET_OS_TV && !TARGET_OS_OSX
139151
if (_hasCustomActivationCriteria) {
140152
// We use "minimumNumberOfTouches" property to prevent pan handler from recognizing
@@ -169,6 +181,7 @@ - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event
169181

170182
- (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event
171183
{
184+
_lastMoveTimestamp = event.timestamp;
172185
[_gestureHandler.pointerTracker touchesMoved:touches withEvent:event];
173186

174187
if (self.state == UIGestureRecognizerStatePossible && [self shouldFailUnderCustomCriteria]) {
@@ -212,6 +225,13 @@ - (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event
212225
#endif
213226
}
214227

228+
// Velocity for the event payload: zero on release when the pointer rested
229+
// before lifting, the recognizer's estimate otherwise.
230+
- (CGPoint)reportedVelocityInView:(RNGHUIView *)view
231+
{
232+
return self.state == UIGestureRecognizerStateEnded && _restedBeforeRelease ? CGPointZero : [self velocityInView:view];
233+
}
234+
215235
- (void)interactionsEnded:(NSSet *)touches withEvent:(UIEvent *)event
216236
{
217237
[_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
@@ -264,6 +284,8 @@ - (void)mouseDragged:(NSEvent *)event
264284

265285
- (void)mouseUp:(NSEvent *)event
266286
{
287+
_restedBeforeRelease = event.timestamp - _lastMoveTimestamp > pointerStoppedInterval;
288+
267289
[super mouseUp:event];
268290
[self interactionsEnded:[NSSet setWithObject:event] withEvent:event];
269291
}
@@ -286,6 +308,8 @@ - (void)touchesMoved:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
286308

287309
- (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
288310
{
311+
_restedBeforeRelease = event.timestamp - _lastMoveTimestamp > pointerStoppedInterval;
312+
289313
[super touchesEnded:touches withEvent:event];
290314
[self interactionsEnded:touches withEvent:event];
291315
}
@@ -306,6 +330,7 @@ - (void)reset
306330
#if TARGET_OS_OSX
307331
_blockAutomaticActivation = NO;
308332
#endif
333+
_restedBeforeRelease = NO;
309334
[super reset];
310335
[_gestureHandler reset];
311336

@@ -486,13 +511,16 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
486511
#if TARGET_OS_OSX
487512
- (RNGestureHandlerEventExtraData *)eventExtraData:(NSPanGestureRecognizer *)recognizer
488513
{
489-
return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:self.coordinateView]
490-
withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView]
491-
withTranslation:[recognizer translationInView:recognizer.view.window.contentView]
492-
withVelocity:[recognizer velocityInView:recognizer.view.window.contentView]
493-
withNumberOfTouches:1
494-
withPointerType:RNGestureHandlerMouse
495-
withStylusData:nil];
514+
RNBetterPanGestureRecognizer *panRecognizer = (RNBetterPanGestureRecognizer *)recognizer;
515+
516+
return
517+
[RNGestureHandlerEventExtraData forPan:[recognizer locationInView:self.coordinateView]
518+
withAbsolutePosition:[recognizer locationInView:recognizer.view.window.contentView]
519+
withTranslation:[recognizer translationInView:recognizer.view.window.contentView]
520+
withVelocity:[panRecognizer reportedVelocityInView:recognizer.view.window.contentView]
521+
withNumberOfTouches:1
522+
withPointerType:RNGestureHandlerMouse
523+
withStylusData:nil];
496524
}
497525
#else
498526
- (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)recognizer
@@ -503,7 +531,7 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)rec
503531
forPan:[recognizer locationInView:self.coordinateView]
504532
withAbsolutePosition:[recognizer locationInView:recognizer.view.window]
505533
withTranslation:[recognizer translationInView:recognizer.view.window]
506-
withVelocity:[recognizer velocityInView:recognizer.view.window]
534+
withVelocity:[panRecognizer reportedVelocityInView:recognizer.view.window]
507535
withNumberOfTouches:recognizer.numberOfTouches
508536
withPointerType:_pointerType
509537
#if !TARGET_OS_TV

0 commit comments

Comments
 (0)