import React, { useEffect, useRef, useCallback, useContext } from 'react'; import { Animated, Easing, LayoutChangeEvent, View } from 'react-native'; import { PanResponderView, PanningProvider, PanningContext, PanningDirections, PanLocationProps, PanDirectionsProps, PanningContextState, PanAmountsProps, } from '../PanningViews'; import constants from '../utils/constants'; const MAXIMUM_DRAGS_AFTER_SWIPE = 2; interface Props { duration: number; children?: React.ReactNode | React.ReactNode[]; direction?: PanningDirections; onDismiss?: () => void; visible?: boolean; ignorePanning?: boolean; } interface LocationProps { left: number; top: number; } // eslint-disable-next-line prettier/prettier const TOP_INSET = constants.isIphoneX ? constants.getSafeAreaInsets().top : constants.isIOS ? 20 : 0; // eslint-disable-next-line prettier/prettier const BOTTOM_INSET = constants.isIphoneX ? constants.getSafeAreaInsets().bottom : constants.isIOS ? 20 : 0; const DialogDismissibleView = (props: Props): JSX.Element => { const { children, direction, onDismiss, visible: propsVisible } = props; const { isPanning, dragDeltas, swipeDirections } = useContext(PanningContext); const width = useRef(constants.screenWidth); const height = useRef(constants.screenHeight); const thresholdX = useRef(0); const thresholdY = useRef(0); const dragsCounter = useRef(0); const animatedValue = useRef(new Animated.Value(0)); const mutableSwipeDirections = useRef({}); const prevDragDeltas = useRef(); const visible = useRef(Boolean(propsVisible)); const getHiddenLocation = useCallback( (left: number, top: number) => { const result = { left: 0, top: 0 }; switch (direction) { case PanningProvider.Directions.LEFT: result.left = -left - width.current; break; case PanningProvider.Directions.RIGHT: result.left = constants.screenWidth - left; break; case PanningProvider.Directions.UP: result.top = -top - height.current - TOP_INSET; break; case PanningProvider.Directions.DOWN: default: result.top = constants.screenHeight - top + BOTTOM_INSET; break; } return result; }, [direction] ); const hiddenLocation = useRef(getHiddenLocation(0, 0)); const animateTo = useCallback( (toValue: number, animationEndCallback?: Animated.EndCallback) => { Animated.timing(animatedValue.current, { toValue, duration: props.duration, easing: Easing.bezier(0.2, 0, 0.35, 1), useNativeDriver: true, }).start(animationEndCallback); }, [props.duration] ); const isSwiping = useCallback(() => { return ( mutableSwipeDirections.current.x !== undefined || mutableSwipeDirections.current.y !== undefined ); }, []); const resetSwipe = useCallback(() => { dragsCounter.current = 0; mutableSwipeDirections.current = {}; }, []); const onDrag = useCallback(() => { if (isSwiping()) { if (dragsCounter.current < MAXIMUM_DRAGS_AFTER_SWIPE) { dragsCounter.current += 1; } else { resetSwipe(); } } }, [isSwiping, resetSwipe]); const hide = useCallback(() => { animateTo(0, () => { visible.current = false; onDismiss?.(); }); }, [animateTo, onDismiss]); useEffect(() => { if ( isPanning && (dragDeltas.x || dragDeltas.y) && (dragDeltas.x !== prevDragDeltas.current?.x || dragDeltas.y !== prevDragDeltas.current?.y) ) { onDrag(); prevDragDeltas.current = dragDeltas; } }, [isPanning, dragDeltas, onDrag, hide]); useEffect(() => { if ( isPanning && (swipeDirections.x || swipeDirections.y) && (swipeDirections.x !== mutableSwipeDirections.current?.x || swipeDirections.y !== mutableSwipeDirections.current?.y) ) { mutableSwipeDirections.current = swipeDirections; } }, [isPanning, swipeDirections, hide]); useEffect(() => { if (visible.current && !propsVisible) { hide(); } }, [propsVisible, hide]); const onLayout = useCallback( (event: LayoutChangeEvent) => { if (propsVisible) { const { layout } = event.nativeEvent; width.current = layout.width; height.current = layout.height; thresholdX.current = width.current / 2; thresholdY.current = height.current / 2; hiddenLocation.current = getHiddenLocation(layout.x, layout.y); animateTo(1); } }, [getHiddenLocation, propsVisible] ); const getAnimationStyle = useCallback(() => { return { transform: [ { translateX: animatedValue.current.interpolate({ inputRange: [0, 1], outputRange: [hiddenLocation.current.left, 0], }), }, { translateY: animatedValue.current.interpolate({ inputRange: [0, 1], outputRange: [hiddenLocation.current.top, 0], }), }, ], }; }, []); const resetToShown = useCallback( (left: number, top: number, _direction?: PanningDirections) => { const toValue = _direction && (_direction === PanningProvider.Directions.LEFT || _direction === PanningProvider.Directions.RIGHT) ? 1 + left / hiddenLocation.current.left : 1 + top / hiddenLocation.current.top; animateTo(toValue); }, [animateTo] ); const onPanLocationChanged = useCallback( ({ left = 0, top = 0 }: PanLocationProps) => { const endValue = { x: Math.round(left), y: Math.round(top) }; if (isSwiping()) { hide(); } else { resetSwipe(); if ( (direction === PanningProvider.Directions.LEFT && endValue.x <= -thresholdX.current) || (direction === PanningProvider.Directions.RIGHT && endValue.x >= thresholdX.current) || (direction === PanningProvider.Directions.UP && endValue.y <= -thresholdY.current) || (direction === PanningProvider.Directions.DOWN && endValue.y >= thresholdY.current) ) { hide(); } else { resetToShown(left, top, direction); } } }, [isSwiping, hide, resetSwipe, direction, resetToShown] ); return ( {children} ); }; export default DialogDismissibleView;