diff --git a/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts index 2a56357853..dd16555735 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts @@ -261,6 +261,12 @@ export default class PanGestureHandler extends GestureHandler { protected override onPointerUp(event: AdaptedEvent): void { this.stylusData = event.stylusData; + // A pointer that rested before lifting reports zero velocity instead of + // the velocity of its last move. + const velocity = this.tracker.getVelocityAt(event.pointerId, event.time); + this.velocityX = velocity?.x ?? 0; + this.velocityY = velocity?.y ?? 0; + super.onPointerUp(event); if (this.state === State.ACTIVE) { const lastCoords = this.tracker.getAbsoluteCoordsAverage(); diff --git a/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts b/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts index c70b1494c7..641d7c4e1e 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts @@ -418,3 +418,45 @@ describe('PanGestureHandler pointer leaving and entering the view', () => { expect(handler.state).toBe(State.CANCELLED); }); }); + +describe('PanGestureHandler velocity at release', () => { + function timedEvent(eventType: EventTypes, y: number, time: number) { + return { ...pointerEvent(eventType, y), time }; + } + + // Four moves 8ms apart, 10px each: 1250px/s downwards. + function dragThenRelease(releaseTime: number) { + const handler = createHandler(); + + handler.setGestureConfig({ enabled: true, minDist: 0 }); + handler.pointerDown(timedEvent(EventTypes.DOWN, 20, 0)); + for (let move = 1; move <= 4; move++) { + handler.pointerMove( + timedEvent(EventTypes.MOVE, 20 + 10 * move, 8 * move) + ); + } + expect(handler.state).toBe(State.ACTIVE); + + handler.pointerUp(timedEvent(EventTypes.UP, 60, releaseTime)); + expect(handler.state).toBe(State.END); + + return handler.nativeEvent(); + } + + test('a pointer released at its last move keeps the velocity of the drag', () => { + expect(dragThenRelease(32).velocityY).toBeCloseTo(1250, -2); + }); + + test('a pointer that rested before release reports zero velocity', () => { + expect(dragThenRelease(532).velocityY).toBe(0); + }); + + // The last move is at 32ms, the pointer counts as stopped after 40ms. + test('a pointer released at the stopped threshold keeps its velocity', () => { + expect(dragThenRelease(72).velocityY).toBeCloseTo(1250, -2); + }); + + test('a pointer released past the stopped threshold reports zero velocity', () => { + expect(dragThenRelease(73).velocityY).toBe(0); + }); +}); diff --git a/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts b/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts index 74fc5ba122..263920166c 100644 --- a/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts +++ b/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts @@ -4,7 +4,6 @@ import VelocityTracker from './VelocityTracker'; export interface TrackerElement { abosoluteCoords: Point; relativeCoords: Point; - timestamp: number; velocityX: number; velocityY: number; } @@ -43,7 +42,6 @@ export default class PointerTracker { const newElement: TrackerElement = { abosoluteCoords: { x: event.x, y: event.y }, relativeCoords: { x: event.offsetX, y: event.offsetY }, - timestamp: event.time, velocityX: 0, velocityY: 0, }; @@ -122,6 +120,15 @@ export default class PointerTracker { : null; } + public getVelocityAt(pointerId: number, time: number) { + if (!this.trackedPointers.has(pointerId)) { + return null; + } + + const [x, y] = this.velocityTracker.velocityAt(time); + return { x, y }; + } + public getLastAbsoluteCoords(pointerId?: number) { return this.trackedPointers.get(pointerId ?? this.lastMovedPointerId) ?.abosoluteCoords; diff --git a/packages/react-native-gesture-handler/src/web/tools/VelocityTracker.ts b/packages/react-native-gesture-handler/src/web/tools/VelocityTracker.ts index 58d45309ea..3569c4a9cf 100644 --- a/packages/react-native-gesture-handler/src/web/tools/VelocityTracker.ts +++ b/packages/react-native-gesture-handler/src/web/tools/VelocityTracker.ts @@ -92,6 +92,17 @@ export default class VelocityTracker { return [0, 0]; } + // Velocity at `time`: zero once no sample arrived within the stopped + // window, the current estimate otherwise. + public velocityAt(time: number): [number, number] { + const newest = this.samples.get(this.samples.size - 1); + + return !newest || + time - newest.time > this.assumePointerMoveStoppedMilliseconds + ? [0, 0] + : this.velocity; + } + public reset(): void { this.samples.clear(); }