import React, { forwardRef, useCallback, useMemo, useRef, useState, } from 'react'; import { InteractionManager, LayoutChangeEvent, ScrollViewProps, StyleSheet, useWindowDimensions, ViewProps, FlatListProps, FlatList, ScrollView, SectionList, SectionListProps, } from 'react-native'; import { NativeViewGestureHandler, PanGestureHandler, PanGestureHandlerGestureEvent, } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedProps, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, withSpring, withTiming, } from 'react-native-reanimated'; import { useMemoizedSharedValue } from './use-memoized-shared-value'; const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); const AnimatedSectionList = Animated.createAnimatedComponent(SectionList); const commonStyles = StyleSheet.create({ flex1: { flex: 1, }, }); const SwipeDismissalView: React.ForwardRefRenderFunction< PanGestureHandler, ViewProps & { onGestureEvent: SwipeDismissalType['onGesture']; nativeHandler: SwipeDismissalType['nativeHandler']; val: Animated.SharedValue; offset: Animated.SharedValue; } > = ( { children, onGestureEvent, nativeHandler, val, offset, style: userPassedStyle, ...props }, ref ) => { const style = useAnimatedStyle(() => { return { flex: 1, transform: [ { translateY: val.value - offset.value, }, ], }; }); return ( {children} ); }; export type SwipeableScrollViewProps = Omit; export type SwipeableFlatListProps = Omit, 'bounces'>; export type SwipeableSectionListProps = Omit, 'bounces'>; const SwipeDismissalScrollView: React.ForwardRefRenderFunction< NativeViewGestureHandler, SwipeableScrollViewProps & { bounces: SwipeDismissalType['bounces']; panHandler: SwipeDismissalType['panHandler']; scrollHandler: SwipeDismissalType['scrollHandler']; } > = ({ bounces, panHandler, scrollHandler, ...props }, ref) => { const animatedProps = useAnimatedProps(() => { return { bounces: bounces.value, }; }); return ( ); }; const SwipeDismissalFlatList: React.ForwardRefRenderFunction< NativeViewGestureHandler, SwipeableFlatListProps & { bounces: SwipeDismissalType['bounces']; panHandler: SwipeDismissalType['panHandler']; scrollHandler: SwipeDismissalType['scrollHandler']; } > = ({ bounces, panHandler, scrollHandler, ...props }, ref) => { const animatedProps = useAnimatedProps(() => { return { bounces: bounces.value, }; }); return ( ); }; const SwipeDismissalSectionList: React.ForwardRefRenderFunction< NativeViewGestureHandler, SwipeableSectionListProps & { bounces: SwipeDismissalType['bounces']; panHandler: SwipeDismissalType['panHandler']; scrollHandler: SwipeDismissalType['scrollHandler']; } > = ({ bounces, panHandler, scrollHandler, ...props }, ref) => { const animatedProps = useAnimatedProps(() => { return { bounces: bounces.value, }; }); return ( ); }; const transition: Animated.WithSpringConfig = { stiffness: 1000, damping: 500, mass: 3, overshootClamping: true, restDisplacementThreshold: 100, restSpeedThreshold: 1000, }; export const useBottomSheet = (config: { onWillOpen?: () => void; onWillClose?: () => void; onDidClose?: () => void; onDidOpen?: () => void; initialPosition: 'open' | 'closed'; }): SwipeDismissalType => { const { onDidClose, onDidOpen, onWillClose, onWillOpen, initialPosition, } = config; const dim = useWindowDimensions(); const canDismiss = useMemoizedSharedValue(0); const scrollStartPosition = useMemoizedSharedValue(0); const val = useMemoizedSharedValue( initialPosition === 'open' ? 0 : dim.height ); const scrollPosition = useMemoizedSharedValue(0); const nativeHandler = useRef(null); const panHandler = useRef(null); const [height, setHeight] = useState(0); const scrollHandler = useAnimatedScrollHandler({ onBeginDrag: (e) => { canDismiss.value = Number(e.contentOffset.y >= -20); scrollStartPosition.value = Math.max(0, e.contentOffset.y); scrollPosition.value = e.contentOffset.y; }, onScroll: (e) => { scrollPosition.value = e.contentOffset.y; }, onEndDrag: () => { scrollStartPosition.value = withTiming(0, { duration: 100 }); }, }); const panTranslation = useMemoizedSharedValue(0); const offset = useDerivedValue(() => { return Math.min(0, scrollStartPosition.value - panTranslation.value); }); const hideAnim = useCallback(() => { if (onWillClose) { runOnJS(onWillClose)(); } const handle = InteractionManager.createInteractionHandle(); val.value = withSpring(height, transition, () => { panTranslation.value = 0; runOnJS(InteractionManager.clearInteractionHandle)(handle); if (onDidClose) { runOnJS(onDidClose)(); } }); }, [height, onDidClose, onWillClose, panTranslation, val]); const showAnim = useCallback(() => { if (onWillOpen) { runOnJS(onWillOpen)(); } const handle = InteractionManager.createInteractionHandle(); val.value = withSpring(0, transition, () => { runOnJS(InteractionManager.clearInteractionHandle)(handle); if (onDidOpen) { runOnJS(onDidOpen)(); } }); }, [onDidOpen, onWillOpen, val]); const onGesture = useAnimatedGestureHandler({ onActive: (e) => { if (canDismiss.value === 0) { return; } panTranslation.value = e.translationY; }, onFinish: (e) => { if (e.velocityY >= 0 && offset.value < -30) { runOnJS(hideAnim)(); } else { panTranslation.value = 0; } }, }); const onLayout = useCallback((e: LayoutChangeEvent) => { setHeight(e.nativeEvent.layout.height); }, []); const SwipeView = useMemo(() => { const Comp = forwardRef(SwipeDismissalView); const WithRef: React.FC = (props) => ( ); return WithRef; }, [offset, onGesture, onLayout, val]); const bounces = useDerivedValue(() => { return offset.value === 0; }); const SheetScrollView = useMemo(() => { const Comp = forwardRef(SwipeDismissalScrollView); const WithRef: React.FC> = (props) => { return ( ); }; return WithRef; }, [bounces, scrollHandler]); const SheetFlatList = useMemo(() => { const Comp = forwardRef(SwipeDismissalFlatList); const WithRef: React.FC, 'bounces'>> = (props) => { return ( ); }; return WithRef; }, [bounces, scrollHandler]); const SheetSectionList = useMemo(() => { const Comp = forwardRef(SwipeDismissalSectionList); const WithRef: React.FC, 'bounces'>> = ( props ) => { return ( ); }; return WithRef; }, [bounces, scrollHandler]); return useMemo( () => ({ scrollHandler, onGesture, nativeHandler, panHandler, SwipeView, SheetScrollView, SheetFlatList, SheetSectionList, bounces, close: hideAnim, open: showAnim, panTranslation, offset, val, scrollPosition: scrollPosition, }), [ scrollHandler, onGesture, SwipeView, SheetScrollView, SheetSectionList, bounces, hideAnim, showAnim, SheetFlatList, panTranslation, offset, val, scrollPosition, ] ); }; const usePanGestureHandler = () => { return useAnimatedGestureHandler({}); }; export type SwipeDismissalType = { scrollHandler: ReturnType; onGesture: ReturnType; nativeHandler: React.RefObject; panHandler: React.RefObject; SwipeView: React.FC; SheetScrollView: React.FC>; SheetFlatList: React.FC, 'bounces'>>; SheetSectionList: React.FC, 'bounces'>>; bounces: Animated.SharedValue; open: () => void; close: () => void; panTranslation: Animated.SharedValue; offset: Animated.SharedValue; val: Animated.SharedValue; scrollPosition: Animated.SharedValue; };