/* @flow */
import * as React from 'react';
import PropTypes from 'prop-types';
import { Animated, StyleSheet, View } from 'react-native';
import { PagerRendererPropType } from './PropTypes';
import type { PagerRendererProps } from './TypeDefinitions';
type Props<T> = PagerRendererProps<T> & {
swipeDistanceThreshold?: number,
swipeVelocityThreshold?: number,
GestureHandler: any,
};
const DefaultTransitionSpec = {
timing: Animated.spring,
tension: 68,
friction: 12,
};
export default class PagerExperimental<T: *> extends React.Component<Props<T>> {
static propTypes = {
...PagerRendererPropType,
swipeDistanceThreshold: PropTypes.number,
swipeVelocityThreshold: PropTypes.number,
GestureHandler: PropTypes.object,
};
static defaultProps = {
canJumpToTab: () => true,
};
componentDidUpdate(prevProps: Props<T>) {
if (
prevProps.navigationState.routes.length !==
this.props.navigationState.routes.length ||
prevProps.layout.width !== this.props.layout.width
) {
this._transitionTo(this.props.navigationState.index, undefined, false);
} else if (
prevProps.navigationState.index !== this.props.navigationState.index &&
this.props.navigationState.index !== this._pendingIndex
) {
this._transitionTo(this.props.navigationState.index);
}
}
_handleHandlerStateChange = event => {
const { GestureHandler } = this.props;
if (event.nativeEvent.state === GestureHandler.State.BEGAN) {
this.props.onSwipeStart && this.props.onSwipeStart();
} else if (event.nativeEvent.state === GestureHandler.State.END) {
this.props.onSwipeEnd && this.props.onSwipeEnd();
const {
navigationState,
layout,
swipeDistanceThreshold = layout.width / 1.75,
swipeVelocityThreshold = 150,
} = this.props;
const {
translationX,
translationY,
velocityX,
velocityY,
} = event.nativeEvent;
const currentIndex =
typeof this._pendingIndex === 'number'
? this._pendingIndex
: navigationState.index;
let nextIndex = currentIndex;
if (
Math.abs(translationX) > Math.abs(translationY) &&
Math.abs(velocityX) > Math.abs(velocityY) &&
(Math.abs(translationX) > swipeDistanceThreshold ||
Math.abs(velocityX) > swipeVelocityThreshold)
) {
nextIndex = Math.round(
Math.min(
Math.max(0, currentIndex - translationX / Math.abs(translationX)),
navigationState.routes.length - 1
)
);
}
if (
!isFinite(nextIndex) ||
!this.props.canJumpToTab(this.props.navigationState.routes[nextIndex])
) {
nextIndex = currentIndex;
}
this._transitionTo(nextIndex, velocityX);
}
};
_transitionTo = (
index: number,
velocity?: number,
animated?: boolean = true
) => {
const offset = -index * this.props.layout.width;
if (this.props.animationEnabled === false || animated === false) {
this.props.panX.setValue(0);
this.props.offsetX.setValue(offset);
return;
}
const { timing, ...transitionConfig } = DefaultTransitionSpec;
const { useNativeDriver } = this.props;
Animated.parallel([
timing(this.props.panX, {
...transitionConfig,
toValue: 0,
velocity,
useNativeDriver,
}),
timing(this.props.offsetX, {
...transitionConfig,
toValue: offset,
velocity,
useNativeDriver,
}),
]).start(({ finished }) => {
if (finished) {
const route = this.props.navigationState.routes[index];
this.props.jumpTo(route.key);
this.props.onAnimationEnd && this.props.onAnimationEnd();
this._pendingIndex = null;
}
});
this._pendingIndex = index;
};
_pendingIndex: ?number;
render() {
const {
GestureHandler,
panX,
offsetX,
layout,
navigationState,
swipeEnabled,
children,
} = this.props;
const { width } = layout;
const { routes } = navigationState;
const maxTranslate = width * (routes.length - 1);
const translateX =
routes.length > 1
? Animated.add(panX, offsetX).interpolate({
inputRange: [-maxTranslate, 0],
outputRange: [-maxTranslate, 0],
extrapolate: 'clamp',
})
: 0;
return (
<GestureHandler.PanGestureHandler
enabled={layout.width !== 0 && swipeEnabled !== false}
minDeltaX={10}
onGestureEvent={Animated.event(
[{ nativeEvent: { translationX: this.props.panX } }],
{ useNativeDriver: this.props.useNativeDriver }
)}
onHandlerStateChange={this._handleHandlerStateChange}
>
<Animated.View
style={[
styles.sheet,
width
? { width: routes.length * width, transform: [{ translateX }] }
: null,
]}
>
{React.Children.map(children, (child, i) => {
const route = navigationState.routes[i];
const focused = i === navigationState.index;
return (
<View
key={route.key}
testID={this.props.getTestID({ route })}
accessibilityElementsHidden={!focused}
importantForAccessibility={
focused ? 'auto' : 'no-hide-descendants'
}
style={
width ? { width } : focused ? StyleSheet.absoluteFill : null
}
>
{focused || width ? child : null}
</View>
);
})}
</Animated.View>
</GestureHandler.PanGestureHandler>
);
}
}
const styles = StyleSheet.create({
sheet: {
flex: 1,
flexDirection: 'row',
alignItems: 'stretch',
},
});