Skip to content

Commit bd9650f

Browse files
authored
Fix minVelocity props behavior (#4327)
## Description `Pan`'s velocity-based activation criteria (`minVelocity`, `minVelocityX`, `minVelocityY`) had three long-standing problems: 1. **Android: `minVelocityY` tested the horizontal velocity.** `shouldActivate()` declared `val vy = velocityY` but compared `vx` in both comparisons of the Y branch, so a pan configured with only `minVelocityY` activated on fast horizontal movement and never on vertical movement. The typo predates the 2022 repo restructure (#2270). 2. **All platforms: per-axis thresholds were compared with sign.** A positive `minVelocityY` only activated on downward movement, negative only on upward. This is surprising — `minVelocityY: 100` reads as "vertical speed of at least 100", not "drag down". The signed logic appears to have been copied from the offset-range checks (`activeOffsetX/Y`), where signed semantics actually make sense. The checks now compare absolute values: `abs(velocity) >= abs(threshold)`, so movement in either direction along the axis activates. (`minVelocity` already compared the velocity vector magnitude and is unchanged.) 3. **Web: `minVelocity` meant something different than on native.** It was mapped onto the per-axis X/Y thresholds ("either axis exceeds the value") instead of the vector magnitude like Android and iOS compute it. The `minVelocitySq` field existed for this but was never assigned from the config. It is now wired up, matching native behavior (e.g. a diagonal drag at 600 pt/s with `minVelocity: 500` now activates on web as it does on native). None of these props were documented anywhere. This PR adds them to the docs (new API + 2.x pages) and to the JSDoc of all three API layers, described as speed "expressed in points per second". > [!WARNING] > I've used [`grep.app`](https://grep.app/) to check whether these props are used and seems that they're not, so it _should be safe_ to introduce these changes. ## Test plan <details> <summary>Tested on the following code:</summary> ```tsx // Repro for Pan minVelocityY issues: // 1. Android compared vx (horizontal velocity) in the minVelocityY branch, // so a fast horizontal swipe wrongly activated the pan. // 2. All platforms compared the velocity with its sign, so a positive // minVelocityY only activated on downward movement. The checks now use // absolute values: either direction along the axis activates. // // Setup: minDistance is set huge so distance can never activate the pan — // only the velocity criteria can. Expected: swiping DOWN or UP faster than // 500 activates; a fast horizontal swipe never does. import React, { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureHandlerRootView, usePanGesture, } from 'react-native-gesture-handler'; export default function EmptyExample() { const [log, setLog] = useState<string[]>([]); const append = (entry: string) => setLog((prev) => [...prev.slice(-8), entry]); const pan = usePanGesture({ minVelocityY: 800, runOnJS: true, onActivate: (e) => { append( `ACTIVATED vx=${Math.round(e.velocityX)} vy=${Math.round(e.velocityY)}` ); }, onFinalize: (e) => { if (e.canceled) { append('finished without activation'); } }, }); return ( <GestureHandlerRootView style={styles.container}> <Text style={styles.title}>minVelocityY = 500, minDistance = 10000</Text> <Text style={styles.hint}> Fast horizontal swipe should NOT activate.{'\n'} Fast vertical swipe (up OR down) SHOULD activate. </Text> <GestureDetector gesture={pan}> <View style={styles.box} testID="panBox"> <Text style={styles.boxLabel}>swipe here</Text> </View> </GestureDetector> <View style={styles.log}> {log.map((entry, i) => ( <Text key={i} style={styles.logLine}> {entry} </Text> ))} </View> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', paddingTop: 60, }, title: { fontSize: 16, fontWeight: 'bold', }, hint: { textAlign: 'center', marginVertical: 12, color: '#666', }, box: { width: 300, height: 300, backgroundColor: 'tomato', borderRadius: 16, justifyContent: 'center', alignItems: 'center', }, boxLabel: { color: 'white', fontSize: 18, }, log: { marginTop: 20, minHeight: 200, alignSelf: 'stretch', paddingHorizontal: 24, }, logLine: { fontFamily: 'monospace', fontSize: 13, }, }); ``` </details>
1 parent ffb9a5f commit bd9650f

10 files changed

Lines changed: 94 additions & 17 deletions

File tree

packages/docs-gesture-handler/docs/gestures/use-pan-gesture.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ minDistance: number | SharedValue<number>;
126126

127127
Minimum distance the finger (or multiple fingers) need to travel before the gesture activates. Expressed in points.
128128

129+
### minVelocity
130+
131+
```ts
132+
minVelocity: number | SharedValue<number>;
133+
```
134+
135+
Minimum speed the pointer has to reach in order to activate the gesture. Expressed in points per second.
136+
137+
### minVelocityX
138+
139+
```ts
140+
minVelocityX: number | SharedValue<number>;
141+
```
142+
143+
Minimum speed along X axis the pointer has to reach in order to activate the gesture. Expressed in points per second.
144+
145+
### minVelocityY
146+
147+
```ts
148+
minVelocityY: number | SharedValue<number>;
149+
```
150+
151+
Minimum speed along Y axis the pointer has to reach in order to activate the gesture. Expressed in points per second.
152+
129153
### minPointers
130154

131155
```ts

packages/docs-gesture-handler/versioned_docs/version-2.x/gesture-handlers/pan-gh.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ See [set of properties inherited from base handler class](/docs/2.x/gesture-hand
5252

5353
Minimum distance the finger (or multiple finger) need to travel before the handler [activates](/docs/2.x/under-the-hood/state#active). Expressed in points.
5454

55+
### `minVelocity`
56+
57+
Minimum speed the pointer has to reach in order for the handler to [activate](/docs/2.x/under-the-hood/state#active). Expressed in points per second.
58+
59+
### `minVelocityX`
60+
61+
Minimum speed along X axis the pointer has to reach in order for the handler to [activate](/docs/2.x/under-the-hood/state#active). Expressed in points per second.
62+
63+
### `minVelocityY`
64+
65+
Minimum speed along Y axis the pointer has to reach in order for the handler to [activate](/docs/2.x/under-the-hood/state#active). Expressed in points per second.
66+
5567
### `minPointers`
5668

5769
A number of fingers that is required to be placed before handler can [activate](/docs/2.x/under-the-hood/state#active). Should be an integer greater than or equal to 0.

packages/docs-gesture-handler/versioned_docs/version-2.x/gestures/pan-gesture.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ If you wish to track the "center of mass" virtual pointer and account for its ch
117117

118118
Minimum distance the finger (or multiple fingers) need to travel before the gesture [activates](/docs/2.x/fundamentals/states-events#active). Expressed in points.
119119

120+
### `minVelocity(value: number)`
121+
122+
Minimum speed the pointer has to reach in order for the gesture to [activate](/docs/2.x/fundamentals/states-events#active). Expressed in points per second.
123+
124+
### `minVelocityX(value: number)`
125+
126+
Minimum speed along X axis the pointer has to reach in order for the gesture to [activate](/docs/2.x/fundamentals/states-events#active). Expressed in points per second.
127+
128+
### `minVelocityY(value: number)`
129+
130+
Minimum speed along Y axis the pointer has to reach in order for the gesture to [activate](/docs/2.x/fundamentals/states-events#active). Expressed in points per second.
131+
120132
### `minPointers(value: number)`
121133

122134
A number of fingers that is required to be placed before the gesture can [activate](/docs/2.x/fundamentals/states-events#active). Should be an integer greater than or equal to 0.

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/PanGestureHandler.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.facebook.react.uimanager.PixelUtil
1111
import com.swmansion.gesturehandler.core.GestureUtils.getLastPointerX
1212
import com.swmansion.gesturehandler.core.GestureUtils.getLastPointerY
1313
import com.swmansion.gesturehandler.react.events.eventbuilders.PanGestureHandlerEventDataBuilder
14+
import kotlin.math.abs
1415

1516
class PanGestureHandler(context: Context?) : GestureHandler() {
1617
override val isContinuous = true
@@ -115,15 +116,11 @@ class PanGestureHandler(context: Context?) : GestureHandler() {
115116
return true
116117
}
117118
val vx = velocityX
118-
if (minVelocityX != MIN_VALUE_IGNORE &&
119-
(minVelocityX < 0 && vx <= minVelocityX || minVelocityX in 0.0f..vx)
120-
) {
119+
if (minVelocityX != MIN_VALUE_IGNORE && abs(vx) >= abs(minVelocityX)) {
121120
return true
122121
}
123122
val vy = velocityY
124-
if (minVelocityY != MIN_VALUE_IGNORE &&
125-
(minVelocityY < 0 && vx <= minVelocityY || minVelocityY in 0.0f..vx)
126-
) {
123+
if (minVelocityY != MIN_VALUE_IGNORE && abs(vy) >= abs(minVelocityY)) {
127124
return true
128125
}
129126
val velocitySq = vx * vx + vy * vy

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,10 @@ - (BOOL)shouldActivateUnderCustomCriteria
337337
}
338338

339339
CGPoint velocity = [self velocityInView:self.view];
340-
if (TEST_MIN_IF_NOT_NAN(velocity.x, _minVelocityX)) {
340+
if (TEST_ABS_MIN_IF_NOT_NAN(velocity.x, _minVelocityX)) {
341341
return YES;
342342
}
343-
if (TEST_MIN_IF_NOT_NAN(velocity.y, _minVelocityY)) {
343+
if (TEST_ABS_MIN_IF_NOT_NAN(velocity.y, _minVelocityY)) {
344344
return YES;
345345
}
346346
if (TEST_MIN_IF_NOT_NAN(VEC_LEN_SQ(velocity), _minVelocitySq)) {

packages/react-native-gesture-handler/apple/RNGestureHandler.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#define TEST_MIN_IF_NOT_NAN(value, limit) \
1616
(!isnan(limit) && ((limit < 0 && value <= limit) || (limit >= 0 && value >= limit)))
1717

18+
#define TEST_ABS_MIN_IF_NOT_NAN(value, limit) (!isnan(limit) && fabs(value) >= fabs(limit))
19+
1820
#define TEST_MAX_IF_NOT_NAN(value, max) (!isnan(max) && ((max < 0 && value < max) || (max >= 0 && value > max)))
1921

2022
#define APPLY_PROP(recognizer, config, type, prop, propName) \

packages/react-native-gesture-handler/src/handlers/PanGestureHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,24 @@ interface CommonPanProperties {
6363
*/
6464
maxPointers?: number;
6565

66+
/**
67+
* Minimum speed the pointer has to reach in order to activate the handler.
68+
* Expressed in points per second.
69+
*/
6670
minVelocity?: number;
71+
72+
/**
73+
* Minimum speed along X axis the pointer has to reach in order to activate
74+
* the handler. Expressed in points per second.
75+
*/
6776
minVelocityX?: number;
77+
78+
/**
79+
* Minimum speed along Y axis the pointer has to reach in order to activate
80+
* the handler. Expressed in points per second.
81+
*/
6882
minVelocityY?: number;
83+
6984
activateAfterLongPress?: number;
7085
}
7186

packages/react-native-gesture-handler/src/handlers/gestures/panGesture.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ export class PanGesture extends ContinousBaseGesture<
154154
}
155155

156156
/**
157-
* Minimum velocity the finger has to reach in order to activate handler.
157+
* Minimum speed the pointer has to reach in order to activate handler.
158+
* Expressed in points per second.
158159
* @param velocity
159160
*/
160161
minVelocity(velocity: number) {
@@ -163,7 +164,8 @@ export class PanGesture extends ContinousBaseGesture<
163164
}
164165

165166
/**
166-
* Minimum velocity along X axis the finger has to reach in order to activate handler.
167+
* Minimum speed along X axis the pointer has to reach in order to activate handler.
168+
* Expressed in points per second.
167169
* @param velocity
168170
*/
169171
minVelocityX(velocity: number) {
@@ -172,7 +174,8 @@ export class PanGesture extends ContinousBaseGesture<
172174
}
173175

174176
/**
175-
* Minimum velocity along Y axis the finger has to reach in order to activate handler.
177+
* Minimum speed along Y axis the pointer has to reach in order to activate handler.
178+
* Expressed in points per second.
176179
* @param velocity
177180
*/
178181
minVelocityY(velocity: number) {

packages/react-native-gesture-handler/src/v3/hooks/gestures/pan/PanTypes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,24 @@ type CommonPanGestureProperties = {
4040
*/
4141
maxPointers?: number;
4242

43+
/**
44+
* Minimum speed the pointer has to reach in order to activate the gesture.
45+
* Expressed in points per second.
46+
*/
4347
minVelocity?: number;
48+
49+
/**
50+
* Minimum speed along X axis the pointer has to reach in order to activate
51+
* the gesture. Expressed in points per second.
52+
*/
4453
minVelocityX?: number;
54+
55+
/**
56+
* Minimum speed along Y axis the pointer has to reach in order to activate
57+
* the gesture. Expressed in points per second.
58+
*/
4559
minVelocityY?: number;
60+
4661
activateAfterLongPress?: number;
4762
};
4863

packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ export default class PanGestureHandler extends GestureHandler {
7575
}
7676

7777
if (config.minVelocity !== undefined) {
78-
this.minVelocityX = config.minVelocity;
79-
this.minVelocityY = config.minVelocity;
78+
this.minVelocitySq = config.minVelocity * config.minVelocity;
8079
this.hasCustomActivationCriteria = true;
8180
}
8281

@@ -432,17 +431,15 @@ export default class PanGestureHandler extends GestureHandler {
432431

433432
if (
434433
this.minVelocityX !== Number.MAX_SAFE_INTEGER &&
435-
((this.minVelocityX < 0 && vx <= this.minVelocityX) ||
436-
(this.minVelocityX >= 0 && this.minVelocityX <= vx))
434+
Math.abs(vx) >= Math.abs(this.minVelocityX)
437435
) {
438436
return true;
439437
}
440438

441439
const vy: number = this.velocityY;
442440
if (
443441
this.minVelocityY !== Number.MAX_SAFE_INTEGER &&
444-
((this.minVelocityY < 0 && vy <= this.minVelocityY) ||
445-
(this.minVelocityY >= 0 && this.minVelocityY <= vy))
442+
Math.abs(vy) >= Math.abs(this.minVelocityY)
446443
) {
447444
return true;
448445
}

0 commit comments

Comments
 (0)