react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
288 lines (287 loc) • 11.6 kB
JavaScript
// Similarily to the DrawerLayout component this deserves to be put in a
// separate repo. Although, keeping it here for the time being will allow us to
// move faster and fix possible issues quicker
import * as React from 'react';
import { Component } from 'react';
import { Animated, StyleSheet, View, I18nManager, } from 'react-native';
import { PanGestureHandler, } from '../handlers/PanGestureHandler';
import { TapGestureHandler } from '../handlers/TapGestureHandler';
import { State } from '../State';
const DRAG_TOSS = 0.05;
/**
* @deprecated use Reanimated version of Swipeable instead
*
* This component allows for implementing swipeable rows or similar interaction.
*/
export default class Swipeable extends Component {
static defaultProps = {
friction: 1,
overshootFriction: 1,
useNativeAnimations: true,
};
constructor(props) {
super(props);
const dragX = new Animated.Value(0);
this.state = {
dragX,
rowTranslation: new Animated.Value(0),
rowState: 0,
leftWidth: undefined,
rightOffset: undefined,
rowWidth: undefined,
};
this.updateAnimatedEvent(props, this.state);
this.onGestureEvent = Animated.event([{ nativeEvent: { translationX: dragX } }], { useNativeDriver: props.useNativeAnimations });
}
shouldComponentUpdate(props, state) {
if (this.props.friction !== props.friction ||
this.props.overshootLeft !== props.overshootLeft ||
this.props.overshootRight !== props.overshootRight ||
this.props.overshootFriction !== props.overshootFriction ||
this.state.leftWidth !== state.leftWidth ||
this.state.rightOffset !== state.rightOffset ||
this.state.rowWidth !== state.rowWidth) {
this.updateAnimatedEvent(props, state);
}
return true;
}
onGestureEvent;
transX;
showLeftAction;
leftActionTranslate;
showRightAction;
rightActionTranslate;
updateAnimatedEvent = (props, state) => {
const { friction, overshootFriction } = props;
const { dragX, rowTranslation, leftWidth = 0, rowWidth = 0 } = state;
const { rightOffset = rowWidth } = state;
const rightWidth = Math.max(0, rowWidth - rightOffset);
const { overshootLeft = leftWidth > 0, overshootRight = rightWidth > 0 } = props;
const transX = Animated.add(rowTranslation, dragX.interpolate({
inputRange: [0, friction],
outputRange: [0, 1],
})).interpolate({
inputRange: [-rightWidth - 1, -rightWidth, leftWidth, leftWidth + 1],
outputRange: [
-rightWidth - (overshootRight ? 1 / overshootFriction : 0),
-rightWidth,
leftWidth,
leftWidth + (overshootLeft ? 1 / overshootFriction : 0),
],
});
this.transX = transX;
this.showLeftAction =
leftWidth > 0
? transX.interpolate({
inputRange: [-1, 0, leftWidth],
outputRange: [0, 0, 1],
})
: new Animated.Value(0);
this.leftActionTranslate = this.showLeftAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp',
});
this.showRightAction =
rightWidth > 0
? transX.interpolate({
inputRange: [-rightWidth, 0, 1],
outputRange: [1, 0, 0],
})
: new Animated.Value(0);
this.rightActionTranslate = this.showRightAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp',
});
};
onTapHandlerStateChange = ({ nativeEvent, }) => {
if (nativeEvent.oldState === State.ACTIVE) {
this.close();
}
};
onHandlerStateChange = (ev) => {
if (ev.nativeEvent.oldState === State.ACTIVE) {
this.handleRelease(ev);
}
if (ev.nativeEvent.state === State.ACTIVE) {
const { velocityX, translationX: dragX } = ev.nativeEvent;
const { rowState } = this.state;
const { friction } = this.props;
const translationX = (dragX + DRAG_TOSS * velocityX) / friction;
const direction = rowState === -1
? 'right'
: rowState === 1
? 'left'
: translationX > 0
? 'left'
: 'right';
if (rowState === 0) {
this.props.onSwipeableOpenStartDrag?.(direction);
}
else {
this.props.onSwipeableCloseStartDrag?.(direction);
}
}
};
handleRelease = (ev) => {
const { velocityX, translationX: dragX } = ev.nativeEvent;
const { leftWidth = 0, rowWidth = 0, rowState } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
const { friction, leftThreshold = leftWidth / 2, rightThreshold = rightWidth / 2, } = this.props;
const startOffsetX = this.currentOffset() + dragX / friction;
const translationX = (dragX + DRAG_TOSS * velocityX) / friction;
let toValue = 0;
if (rowState === 0) {
if (translationX > leftThreshold) {
toValue = leftWidth;
}
else if (translationX < -rightThreshold) {
toValue = -rightWidth;
}
}
else if (rowState === 1) {
// Swiped to left
if (translationX > -leftThreshold) {
toValue = leftWidth;
}
}
else {
// Swiped to right
if (translationX < rightThreshold) {
toValue = -rightWidth;
}
}
this.animateRow(startOffsetX, toValue, velocityX / friction);
};
animateRow = (fromValue, toValue, velocityX) => {
const { dragX, rowTranslation } = this.state;
dragX.setValue(0);
rowTranslation.setValue(fromValue);
this.setState({ rowState: Math.sign(toValue) });
Animated.spring(rowTranslation, {
restSpeedThreshold: 1.7,
restDisplacementThreshold: 0.4,
velocity: velocityX,
bounciness: 0,
toValue,
useNativeDriver: this.props.useNativeAnimations,
...this.props.animationOptions,
}).start(({ finished }) => {
if (finished) {
if (toValue > 0) {
this.props.onSwipeableLeftOpen?.();
this.props.onSwipeableOpen?.('left', this);
}
else if (toValue < 0) {
this.props.onSwipeableRightOpen?.();
this.props.onSwipeableOpen?.('right', this);
}
else {
const closingDirection = fromValue > 0 ? 'left' : 'right';
this.props.onSwipeableClose?.(closingDirection, this);
}
}
});
if (toValue > 0) {
this.props.onSwipeableLeftWillOpen?.();
this.props.onSwipeableWillOpen?.('left');
}
else if (toValue < 0) {
this.props.onSwipeableRightWillOpen?.();
this.props.onSwipeableWillOpen?.('right');
}
else {
const closingDirection = fromValue > 0 ? 'left' : 'right';
this.props.onSwipeableWillClose?.(closingDirection);
}
};
onRowLayout = ({ nativeEvent }) => {
this.setState({ rowWidth: nativeEvent.layout.width });
};
currentOffset = () => {
const { leftWidth = 0, rowWidth = 0, rowState } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
if (rowState === 1) {
return leftWidth;
}
else if (rowState === -1) {
return -rightWidth;
}
return 0;
};
close = () => {
this.animateRow(this.currentOffset(), 0);
};
// eslint-disable-next-line @eslint-react/no-unused-class-component-members
openLeft = () => {
const { leftWidth = 0 } = this.state;
this.animateRow(this.currentOffset(), leftWidth);
};
// eslint-disable-next-line @eslint-react/no-unused-class-component-members
openRight = () => {
const { rowWidth = 0 } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
this.animateRow(this.currentOffset(), -rightWidth);
};
// eslint-disable-next-line @eslint-react/no-unused-class-component-members
reset = () => {
const { dragX, rowTranslation } = this.state;
dragX.setValue(0);
rowTranslation.setValue(0);
this.setState({ rowState: 0 });
};
render() {
const { rowState } = this.state;
const { children, renderLeftActions, renderRightActions, dragOffsetFromLeftEdge = 10, dragOffsetFromRightEdge = 10, } = this.props;
const left = renderLeftActions && (<Animated.View style={[
styles.leftActions,
// All those and below parameters can have ! since they are all
// asigned in constructor in `updateAnimatedEvent` but TS cannot spot
// it for some reason
{ transform: [{ translateX: this.leftActionTranslate }] },
]}>
{renderLeftActions(this.showLeftAction, this.transX, this)}
<View onLayout={({ nativeEvent }) => this.setState({ leftWidth: nativeEvent.layout.x })}/>
</Animated.View>);
const right = renderRightActions && (<Animated.View style={[
styles.rightActions,
{ transform: [{ translateX: this.rightActionTranslate }] },
]}>
{renderRightActions(this.showRightAction, this.transX, this)}
<View onLayout={({ nativeEvent }) => this.setState({ rightOffset: nativeEvent.layout.x })}/>
</Animated.View>);
return (<PanGestureHandler activeOffsetX={[-dragOffsetFromRightEdge, dragOffsetFromLeftEdge]} touchAction="pan-y" {...this.props} onGestureEvent={this.onGestureEvent} onHandlerStateChange={this.onHandlerStateChange}>
<Animated.View onLayout={this.onRowLayout} style={[styles.container, this.props.containerStyle]}>
{left}
{right}
<TapGestureHandler enabled={rowState !== 0} touchAction="pan-y" onHandlerStateChange={this.onTapHandlerStateChange}>
<Animated.View pointerEvents={rowState === 0 ? 'auto' : 'box-only'} style={[
{
transform: [{ translateX: this.transX }],
},
this.props.childrenContainerStyle,
]}>
{children}
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>);
}
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
leftActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
rightActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse',
},
});