Skip to content
This repository was archived by the owner on Nov 27, 2022. It is now read-only.

Commit e7f832c

Browse files
authored
feat: consider both velocity and position while calculating index (#802)
Motivation Previously evaluating next index was based only on position OR velocity. It wasn't considering BOTH of them which appears to natural behavior. I believe that the expected scenario is that we can calculate is component left without snapping will rather stop decaying on the current or next/previous page. I figured out (calculation attached) that final position (actually delta of current and final extrapolated) is ~ to v^2 and knowing that I implemented a more physical-based model for evaluating next index. Changes I changed the implementation of calculating the next index. Also, I had to drop swipeVelocityThreshold and swipeDistanceThreshold and then I replaced it with swipeVelocityImpact. I think it's a good time for this move bc it's not very breaking and this version is not yet by default in react-navigation. swipeVelocityImpact is a factor in delta pos = a * v^2
1 parent cc09bc1 commit e7f832c

File tree

4 files changed

+41
-67
lines changed

4 files changed

+41
-67
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,9 @@ String indicating whether the keyboard gets dismissed in response to a drag gest
287287

288288
Boolean indicating whether to enable swipe gestures. Swipe gestures are enabled by default. Passing `false` will disable swipe gestures, but the user can still switch tabs by pressing the tab bar.
289289

290-
##### `swipeDistanceThreshold`
290+
##### `swipeVelocityImpact`
291291

292-
Minimum swipe distance which triggers a tab switch. By default, this is automatically determined based on the screen width.
293-
294-
##### `swipeVelocityThreshold`
295-
296-
Minimum swipe velocity which triggers a tab switch. Defaults to `1200`.
292+
Determines how relevant is a velocity while calculating next position while swiping. Defaults to `0.01`.
297293

298294
##### `onSwipeStart`
299295

src/Pager.tsx

Lines changed: 36 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const {
4444
onChange,
4545
abs,
4646
add,
47-
and,
4847
block,
4948
call,
5049
ceil,
@@ -60,7 +59,6 @@ const {
6059
min,
6160
multiply,
6261
neq,
63-
or,
6462
not,
6563
round,
6664
set,
@@ -80,9 +78,8 @@ const DIRECTION_LEFT = 1;
8078
const DIRECTION_RIGHT = -1;
8179

8280
const SWIPE_DISTANCE_MINIMUM = 20;
83-
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;
8481

85-
const SWIPE_VELOCITY_THRESHOLD_DEFAULT = 800;
82+
const SWIPE_VELOCITY_IMPACT = 800;
8683

8784
const SPRING_CONFIG = {
8885
stiffness: 1000,
@@ -100,15 +97,14 @@ const TIMING_CONFIG = {
10097

10198
export default class Pager<T extends Route> extends React.Component<Props<T>> {
10299
static defaultProps = {
103-
swipeVelocityThreshold: SWIPE_VELOCITY_THRESHOLD_DEFAULT,
100+
swipeVelocityImpact: SWIPE_VELOCITY_IMPACT,
104101
};
105102

106103
componentDidUpdate(prevProps: Props<T>) {
107104
const {
108105
navigationState,
109106
layout,
110-
swipeDistanceThreshold,
111-
swipeVelocityThreshold,
107+
swipeVelocityImpact,
112108
springConfig,
113109
timingConfig,
114110
} = this.props;
@@ -139,23 +135,11 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
139135
this.layoutWidth.setValue(layout.width);
140136
}
141137

142-
if (swipeDistanceThreshold != null) {
143-
if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
144-
this.swipeDistanceThreshold.setValue(swipeDistanceThreshold);
145-
}
146-
} else {
147-
if (prevProps.layout.width !== layout.width) {
148-
this.swipeDistanceThreshold.setValue(
149-
layout.width * SWIPE_DISTANCE_MULTIPLIER
150-
);
151-
}
152-
}
153-
154-
if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
155-
this.swipeVelocityThreshold.setValue(
156-
swipeVelocityThreshold != null
157-
? swipeVelocityThreshold
158-
: SWIPE_VELOCITY_THRESHOLD_DEFAULT
138+
if (prevProps.swipeVelocityImpact !== swipeVelocityImpact) {
139+
this.swipeVelocityImpact.setValue(
140+
swipeVelocityImpact != null
141+
? swipeVelocityImpact
142+
: SWIPE_VELOCITY_IMPACT
159143
);
160144
}
161145

@@ -204,7 +188,7 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
204188
// Current state of the gesture
205189
private velocityX = new Value(0);
206190
private gestureX = new Value(0);
207-
private gestureState = new Value(State.UNDETERMINED);
191+
private gestureState = new Value(State.END);
208192
private offsetX = new Value(0);
209193

210194
// Current progress of the page (translateX value)
@@ -235,11 +219,10 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
235219
private routesLength = new Value(this.props.navigationState.routes.length);
236220
private layoutWidth = new Value(this.props.layout.width);
237221

238-
// Threshold values to determine when to trigger a swipe gesture
239-
private swipeDistanceThreshold = new Value(
240-
this.props.swipeDistanceThreshold || 180
222+
// Determines how relevant is a velocity while calculating next position while swiping
223+
private swipeVelocityImpact = new Value(
224+
this.props.swipeVelocityImpact || SWIPE_VELOCITY_IMPACT
241225
);
242-
private swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);
243226

244227
// The position value represent the position of the pager on a scale of 0 - routes.length-1
245228
// It is calculated based on the translate value and layout width
@@ -427,6 +410,21 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
427410
},
428411
]);
429412

413+
private velocitySignum = cond(
414+
this.velocityX,
415+
divide(abs(this.velocityX), this.velocityX),
416+
0
417+
);
418+
private extrapolatedPosition = add(
419+
this.gestureX,
420+
multiply(
421+
this.velocityX,
422+
this.velocityX,
423+
this.velocitySignum,
424+
this.swipeVelocityImpact
425+
)
426+
);
427+
430428
private translateX = block([
431429
onChange(
432430
this.index,
@@ -516,16 +514,14 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
516514
// Stop animations while we're dragging
517515
stopClock(this.clock),
518516
],
519-
[
517+
518+
cond(eq(this.gestureState, State.END), [
520519
set(this.isSwiping, FALSE),
521520
this.transitionTo(
522521
cond(
523-
and(
524-
greaterThan(abs(this.gestureX), SWIPE_DISTANCE_MINIMUM),
525-
or(
526-
greaterThan(abs(this.gestureX), this.swipeDistanceThreshold),
527-
greaterThan(abs(this.velocityX), this.swipeVelocityThreshold)
528-
)
522+
greaterThan(
523+
abs(this.extrapolatedPosition),
524+
divide(this.layoutWidth, 2)
529525
),
530526
// For swipe gesture, to calculate the index, determine direction and add to index
531527
// When the user swipes towards the left, we transition to the next tab
@@ -537,24 +533,9 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
537533
sub(
538534
this.index,
539535
cond(
540-
greaterThan(
541-
// Gesture can be positive, or negative
542-
// Get absolute for comparision
543-
abs(this.gestureX),
544-
this.swipeDistanceThreshold
545-
),
546-
// If gesture value exceeded the threshold, calculate direction from distance travelled
547-
cond(
548-
greaterThan(this.gestureX, 0),
549-
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
550-
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
551-
),
552-
// Otherwise calculate direction from the gesture velocity
553-
cond(
554-
greaterThan(this.velocityX, 0),
555-
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
556-
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
557-
)
536+
greaterThan(this.velocitySignum, 0),
537+
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
538+
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
558539
)
559540
)
560541
),
@@ -565,7 +546,7 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
565546
this.index
566547
)
567548
),
568-
]
549+
])
569550
),
570551
this.progress,
571552
]);

src/TabView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ export default class TabView<T extends Route> extends React.Component<
107107
removeClippedSubviews,
108108
keyboardDismissMode,
109109
swipeEnabled,
110-
swipeDistanceThreshold,
111-
swipeVelocityThreshold,
110+
swipeVelocityImpact,
112111
timingConfig,
113112
springConfig,
114113
tabBarPosition,
@@ -128,8 +127,7 @@ export default class TabView<T extends Route> extends React.Component<
128127
layout={layout}
129128
keyboardDismissMode={keyboardDismissMode}
130129
swipeEnabled={swipeEnabled}
131-
swipeDistanceThreshold={swipeDistanceThreshold}
132-
swipeVelocityThreshold={swipeVelocityThreshold}
130+
swipeVelocityImpact={swipeVelocityImpact}
133131
timingConfig={timingConfig}
134132
springConfig={springConfig}
135133
onSwipeStart={onSwipeStart}

src/types.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ export type EventEmitterProps = {
3939
export type PagerCommonProps = {
4040
keyboardDismissMode: 'none' | 'on-drag';
4141
swipeEnabled: boolean;
42-
swipeDistanceThreshold?: number;
43-
swipeVelocityThreshold?: number;
42+
swipeVelocityImpact?: number;
4443
onSwipeStart?: () => void;
4544
onSwipeEnd?: () => void;
4645
springConfig: {

0 commit comments

Comments
 (0)