import React, { useEffect, useRef, useState } from 'react'; import { Animated, PanResponder, Easing, Platform } from 'react-native'; import type { ReactElement, ReactNode } from 'react'; import { StyledDragableContainer, StyledDragableDrawerContainer, StyledHandler, StyledHandlerContainer, } from '../StyledDrawer'; import { calculateAnimatedToValue, calculateSnapPointsData, getOffset, } from './helpers'; import type { SnapPointsData } from './helpers'; export interface DragableDrawerProps { /** * Drawer's content. */ children?: ReactNode; /** * Initial visible height percentage of the drawer. Can be used to programmatically control the drawer height. Range: [0,100]. */ initialHeightPercentage?: number; /** * Mininum visible height percentage of the drawer. This will be the lowest point in snap points list. Range: [0,100]. */ minimumHeightPercentage?: number; /** * Callback function is called when the drawer expand to max position. */ onExpanded?: () => void; /** * Callback function is called when the drawer collapsed to min position. */ onCollapsed?: () => void; /** * Testing id of the component. */ testID?: string; /** * Nearest height snap points that the drawer will attach to on release gesture. Range: [0,100] */ snapPoints?: number[]; } const DragableDrawer = ({ children, initialHeightPercentage, minimumHeightPercentage = 10, snapPoints = [], onExpanded, onCollapsed, testID, }: DragableDrawerProps): ReactElement => { const [height, setHeight] = useState(0); const baseHeightForMeasure = useRef(0); const snapPointsData = useRef({ list: [], minHeightOffset: 0, maxHeightOffset: 0, }); // Track drag const pan = useRef(new Animated.Value(0)).current; const offset = useRef(0); const offsetBeforePan = useRef(0); const [animatedToValue, setAnimatedToValue] = useState(-1); useEffect(() => { const id = pan.addListener(({ value }) => { offset.current = value; }); return () => pan.removeListener(id); }, []); useEffect(() => { if (height > 0) { const initialOffset = getOffset( height, initialHeightPercentage || minimumHeightPercentage ); setAnimatedToValue(initialOffset); } }, [height]); useEffect(() => { if (height > 0) { pan.setValue(height); offset.current = height; baseHeightForMeasure.current = height; // Calculate snap points information snapPointsData.current = calculateSnapPointsData( minimumHeightPercentage, height, snapPoints ); } }, [height, minimumHeightPercentage]); useEffect(() => { if (animatedToValue >= 0) { const animation = Animated.timing(pan, { toValue: animatedToValue, useNativeDriver: Platform.OS !== 'web', easing: Easing.inOut(Easing.cubic), }); animation.start(() => { if (animatedToValue === 0) { onExpanded?.(); } else if ( animatedToValue === getOffset(height, minimumHeightPercentage) ) { onCollapsed?.(); } setAnimatedToValue(-1); }); return () => animation.stop(); } }, [animatedToValue]); const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, onPanResponderGrant: () => { offsetBeforePan.current = offset.current; pan.setOffset(offset.current); pan.setValue(0); }, onPanResponderMove: (_, gesture) => { const panDistance = gesture.dy; // Moving toward top, stop at highest snap point if (offsetBeforePan.current + panDistance < 0) { pan.setValue(-offsetBeforePan.current); return; } // Moving toward bottom, stop at lowest snap point if ( offsetBeforePan.current + panDistance > snapPointsData.current?.minHeightOffset ) { pan.setValue( baseHeightForMeasure.current - baseHeightForMeasure.current * (minimumHeightPercentage / 100) - offsetBeforePan.current ); return; } pan.setValue(panDistance); }, onPanResponderRelease: (_, gesture) => { pan.flattenOffset(); // Attach to nearest snappoint const panDistance = gesture.dy; const offsetAfterPan = offsetBeforePan.current + panDistance; const animatedValue = calculateAnimatedToValue( offsetAfterPan, snapPointsData.current.list ); setAnimatedToValue(animatedValue); }, }) ).current; return ( 0 ? 1 : 0 }, { translateY: pan }, ], }} onLayout={({ nativeEvent }) => { setHeight(nativeEvent.layout.height); }} > {children} ); }; export default DragableDrawer;