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

Commit

Permalink
feat: consider both velocity and position while calculating index (#802)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
osdnk committed Jun 12, 2019
1 parent cc09bc1 commit e7f832c
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 67 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,9 @@ String indicating whether the keyboard gets dismissed in response to a drag gest

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.

##### `swipeDistanceThreshold`
##### `swipeVelocityImpact`

Minimum swipe distance which triggers a tab switch. By default, this is automatically determined based on the screen width.

##### `swipeVelocityThreshold`

Minimum swipe velocity which triggers a tab switch. Defaults to `1200`.
Determines how relevant is a velocity while calculating next position while swiping. Defaults to `0.01`.

##### `onSwipeStart`

Expand Down
91 changes: 36 additions & 55 deletions src/Pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const {
onChange,
abs,
add,
and,
block,
call,
ceil,
Expand All @@ -60,7 +59,6 @@ const {
min,
multiply,
neq,
or,
not,
round,
set,
Expand All @@ -80,9 +78,8 @@ const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;

const SWIPE_DISTANCE_MINIMUM = 20;
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;

const SWIPE_VELOCITY_THRESHOLD_DEFAULT = 800;
const SWIPE_VELOCITY_IMPACT = 800;

const SPRING_CONFIG = {
stiffness: 1000,
Expand All @@ -100,15 +97,14 @@ const TIMING_CONFIG = {

export default class Pager<T extends Route> extends React.Component<Props<T>> {
static defaultProps = {
swipeVelocityThreshold: SWIPE_VELOCITY_THRESHOLD_DEFAULT,
swipeVelocityImpact: SWIPE_VELOCITY_IMPACT,
};

componentDidUpdate(prevProps: Props<T>) {
const {
navigationState,
layout,
swipeDistanceThreshold,
swipeVelocityThreshold,
swipeVelocityImpact,
springConfig,
timingConfig,
} = this.props;
Expand Down Expand Up @@ -139,23 +135,11 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
this.layoutWidth.setValue(layout.width);
}

if (swipeDistanceThreshold != null) {
if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
this.swipeDistanceThreshold.setValue(swipeDistanceThreshold);
}
} else {
if (prevProps.layout.width !== layout.width) {
this.swipeDistanceThreshold.setValue(
layout.width * SWIPE_DISTANCE_MULTIPLIER
);
}
}

if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
this.swipeVelocityThreshold.setValue(
swipeVelocityThreshold != null
? swipeVelocityThreshold
: SWIPE_VELOCITY_THRESHOLD_DEFAULT
if (prevProps.swipeVelocityImpact !== swipeVelocityImpact) {
this.swipeVelocityImpact.setValue(
swipeVelocityImpact != null
? swipeVelocityImpact
: SWIPE_VELOCITY_IMPACT
);
}

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

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

// Threshold values to determine when to trigger a swipe gesture
private swipeDistanceThreshold = new Value(
this.props.swipeDistanceThreshold || 180
// Determines how relevant is a velocity while calculating next position while swiping
private swipeVelocityImpact = new Value(
this.props.swipeVelocityImpact || SWIPE_VELOCITY_IMPACT
);
private swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);

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

private velocitySignum = cond(
this.velocityX,
divide(abs(this.velocityX), this.velocityX),
0
);
private extrapolatedPosition = add(
this.gestureX,
multiply(
this.velocityX,
this.velocityX,
this.velocitySignum,
this.swipeVelocityImpact
)
);

private translateX = block([
onChange(
this.index,
Expand Down Expand Up @@ -516,16 +514,14 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
// Stop animations while we're dragging
stopClock(this.clock),
],
[

cond(eq(this.gestureState, State.END), [
set(this.isSwiping, FALSE),
this.transitionTo(
cond(
and(
greaterThan(abs(this.gestureX), SWIPE_DISTANCE_MINIMUM),
or(
greaterThan(abs(this.gestureX), this.swipeDistanceThreshold),
greaterThan(abs(this.velocityX), this.swipeVelocityThreshold)
)
greaterThan(
abs(this.extrapolatedPosition),
divide(this.layoutWidth, 2)
),
// For swipe gesture, to calculate the index, determine direction and add to index
// When the user swipes towards the left, we transition to the next tab
Expand All @@ -537,24 +533,9 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
sub(
this.index,
cond(
greaterThan(
// Gesture can be positive, or negative
// Get absolute for comparision
abs(this.gestureX),
this.swipeDistanceThreshold
),
// If gesture value exceeded the threshold, calculate direction from distance travelled
cond(
greaterThan(this.gestureX, 0),
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
),
// Otherwise calculate direction from the gesture velocity
cond(
greaterThan(this.velocityX, 0),
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
)
greaterThan(this.velocitySignum, 0),
I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
)
)
),
Expand All @@ -565,7 +546,7 @@ export default class Pager<T extends Route> extends React.Component<Props<T>> {
this.index
)
),
]
])
),
this.progress,
]);
Expand Down
6 changes: 2 additions & 4 deletions src/TabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ export default class TabView<T extends Route> extends React.Component<
removeClippedSubviews,
keyboardDismissMode,
swipeEnabled,
swipeDistanceThreshold,
swipeVelocityThreshold,
swipeVelocityImpact,
timingConfig,
springConfig,
tabBarPosition,
Expand All @@ -128,8 +127,7 @@ export default class TabView<T extends Route> extends React.Component<
layout={layout}
keyboardDismissMode={keyboardDismissMode}
swipeEnabled={swipeEnabled}
swipeDistanceThreshold={swipeDistanceThreshold}
swipeVelocityThreshold={swipeVelocityThreshold}
swipeVelocityImpact={swipeVelocityImpact}
timingConfig={timingConfig}
springConfig={springConfig}
onSwipeStart={onSwipeStart}
Expand Down
3 changes: 1 addition & 2 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export type EventEmitterProps = {
export type PagerCommonProps = {
keyboardDismissMode: 'none' | 'on-drag';
swipeEnabled: boolean;
swipeDistanceThreshold?: number;
swipeVelocityThreshold?: number;
swipeVelocityImpact?: number;
onSwipeStart?: () => void;
onSwipeEnd?: () => void;
springConfig: {
Expand Down

0 comments on commit e7f832c

Please sign in to comment.