// @flow // 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 React, { Component } from 'react'; import { Animated, StyleSheet, View } from 'react-native'; import { AnimatedEvent } from 'react-native/Libraries/Animated/src/AnimatedEvent'; import { PanGestureHandler, TapGestureHandler, State, } from 'react-native-gesture-handler'; const DRAG_TOSS = 0.05; // Math.sign polyfill for iOS 8.x if (!Math.sign) { Math.sign = function(x) { return ((x > 0) - (x < 0)) || +x; }; } export type PropType = { children: any, friction: number, leftThreshold?: number, rightThreshold?: number, overshootLeft?: boolean, overshootRight?: boolean, onSwipeableLeftOpen?: Function, onSwipeableRightOpen?: Function, onSwipeableOpen?: Function, onSwipeableClose?: Function, renderLeftActions?: ( progressAnimatedValue: any, dragAnimatedValue: any ) => any, renderRightActions?: ( progressAnimatedValue: any, dragAnimatedValue: any ) => any, useNativeAnimations: boolean, }; type StateType = { dragX: Animated.Value, rowTranslation: Animated.Value, rowState: number, leftWidth: number | typeof undefined, rightOffset: number | typeof undefined, rowWidth: number | typeof undefined, }; export default class Swipeable extends Component { static defaultProps = { friction: 1, useNativeAnimations: true, }; _onGestureEvent: ?AnimatedEvent; _transX: ?Animated.Interpolation; _showLeftAction: ?Animated.Interpolation; _leftActionTranslate: ?Animated.Interpolation; _showRightAction: ?Animated.Interpolation; _rightActionTranslate: ?Animated.Interpolation; constructor(props: PropType) { 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 } ); } componentWillUpdate(props: PropType, state: StateType) { if ( this.props.friction !== props.friction || this.props.overshootLeft !== props.overshootLeft || this.props.overshootRight !== props.overshootRight || this.state.leftWidth !== state.leftWidth || this.state.rightOffset !== state.rightOffset || this.state.rowWidth !== state.rowWidth ) { this._updateAnimatedEvent(props, state); } } _updateAnimatedEvent = (props: PropType, state: StateType) => { const { friction, useNativeAnimations } = 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 : 0), -rightWidth, leftWidth, leftWidth + (overshootLeft ? 1 : 0), ], }); this._transX = transX; this._showLeftAction = transX.interpolate({ inputRange: [-1, 0, leftWidth], outputRange: [0, 0, 1], extrapolate: 'clamp', }); this._leftActionTranslate = this._showLeftAction.interpolate({ inputRange: [0, Number.MIN_VALUE], outputRange: [-10000, 0], extrapolate: 'clamp', }); this._showRightAction = transX.interpolate({ inputRange: [-rightWidth, 0, 1], outputRange: [1, 0, 0], extrapolate: 'clamp', }); 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 = ({ nativeEvent }) => { if (nativeEvent.oldState === State.ACTIVE) { this._handleRelease(nativeEvent); } }; _handleRelease = nativeEvent => { const { velocityX, translationX: dragX } = 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, { velocity: velocityX, bounciness: 0, toValue, useNativeDriver: this.props.useNativeAnimations, }).start(({ finished }) => { if (finished) { if (toValue > 0 && this.props.onSwipeableLeftOpen) { this.props.onSwipeableLeftOpen(); } else if (toValue < 0 && this.props.onSwipeableRightOpen) { this.props.onSwipeableRightOpen(); } if (toValue === 0) { this.props.onSwipeableClose && this.props.onSwipeableClose(); } else { this.props.onSwipeableOpen && this.props.onSwipeableOpen(); } } }); }; _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); }; render() { const { rowState } = this.state; const { children, renderLeftActions, renderRightActions } = this.props; const left = renderLeftActions && ( {renderLeftActions(this._showLeftAction, this._transX)} this.setState({ leftWidth: nativeEvent.layout.x })} /> ); const right = renderRightActions && ( {renderRightActions(this._showRightAction, this._transX)} this.setState({ rightOffset: nativeEvent.layout.x })} /> ); return ( {left} {right} {children} ); } } const styles = StyleSheet.create({ container: { overflow: 'hidden', }, leftActions: { ...StyleSheet.absoluteFillObject, flexDirection: 'row', }, rightActions: { ...StyleSheet.absoluteFillObject, flexDirection: 'row-reverse', }, });