import React, { memo, useRef } from 'react'; import { PanResponder, Animated, GestureResponderEvent, PanResponderGestureState, LayoutChangeEvent, LayoutRectangle, StyleProp, ViewStyle, } from 'react-native'; import constants from '../utils/constants'; // eslint-disable-next-line no-shadow export enum GestureDirections { // VERTICAL UP = 'up', DOWN = 'down', } export interface PanGestureViewProps { style?: StyleProp; onDismiss?: () => void; // 允许移动的方向 (默认是 down) direction?: GestureDirections; children?: React.ReactNode; } const SWIPE_VELOCITY = 1.8; const SPEED = 20; const BOUNCINESS = 6; const PanGestureView = (props: PanGestureViewProps): JSX.Element => { const { children, direction = GestureDirections.DOWN, style } = props; const swipe = useRef(); const layout = useRef(); const deltaY = useRef(new Animated.Value(0)).current; const handleMoveShouldSetPanResponder = ( _e: GestureResponderEvent, gestureState: PanResponderGestureState ): boolean => { // return true if user is swiping, return false if it's a single click const { dy } = gestureState; return dy > 5 || dy < -5; }; const handlePanResponderGrant = () => { swipe.current = false; }; const handlePanResponderMove = ( _e: GestureResponderEvent, gestureState: PanResponderGestureState ) => { let newValue = 0; // VERTICAL const up = direction === GestureDirections.UP; const panDeltaY = gestureState.dy; const panVelocityY = gestureState.vy; if (Math.abs(panVelocityY) >= SWIPE_VELOCITY) { if ((up && panVelocityY < 0) || (!up && panVelocityY > 0)) { // Swipe swipe.current = true; } } else if ((up && panDeltaY < 0) || (!up && panDeltaY > 0)) { // Drag newValue = panDeltaY; animateDeltaY(Math.round(newValue)); } }; const onAnimatedFinished = ({ finished }: { finished: boolean }) => { if (finished) { onDismiss(); } }; const initPositions = () => { deltaY.setValue(0); }; const onDismiss = () => { initPositions(); props.onDismiss?.(); }; const animateDeltaY = (toValue: number) => { Animated.spring(deltaY, { toValue, useNativeDriver: true, speed: SPEED, bounciness: BOUNCINESS, }).start(); }; const animateDismiss = () => { // VERTICAL const up = direction === GestureDirections.UP; const newValue = up ? -layout!.current!.height - constants.statusBarHeight : // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore deltaY._value + constants.screenHeight; Animated.timing(deltaY, { toValue: Math.round(newValue), useNativeDriver: true, duration: 280, }).start(onAnimatedFinished); }; const handlePanResponderEnd = () => { if (!swipe.current) { // VERTICAL const up = direction === GestureDirections.UP; const threshold = layout!.current!.height / 2; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const endValue = Math.round(deltaY._value); if ((up && endValue <= -threshold) || (!up && endValue >= threshold)) { animateDismiss(); } else { // back to initial position animateDeltaY(0); } } else { animateDismiss(); } }; const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder, onPanResponderGrant: handlePanResponderGrant, onPanResponderMove: handlePanResponderMove, onPanResponderRelease: handlePanResponderEnd, onPanResponderTerminate: handlePanResponderEnd, }) ).current; const onLayout = (event: LayoutChangeEvent) => { layout.current = event.nativeEvent.layout; }; return ( {children} ); }; PanGestureView.displayName = 'PanGestureView'; PanGestureView.directions = GestureDirections; export default memo(PanGestureView);