import type { ReactNode } from 'react' import type { DimensionValue, ViewStyle } from 'react-native' import React, { useEffect, useMemo, useState } from 'react' import { Dimensions, StyleSheet, Text, View } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import Animated, { interpolate, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated' import LucideIcon from './LucideIcon' const SCREEN = Dimensions.get('screen') interface DrawerMenuProps { visible: boolean duration?: number menuPosition?: 'left' | 'right' | 'top' | 'bottom' height?: DimensionValue hideModal: () => void children?: ReactNode header?: ReactNode isCircle?: boolean CircleNum?: number isShadowHideModal?: boolean title?: string showHeader?: boolean } export default function Drawer({ visible, height = 300, hideModal, duration = 300, isCircle, menuPosition = 'bottom', children, CircleNum = 12, isShadowHideModal = true, header, title, showHeader = true, }: DrawerMenuProps) { const progress = useSharedValue(0) const drag = useSharedValue(0) const [innerVisible, setInnerVisible] = useState(false) /* ================= 显隐 ================= */ useEffect(() => { if (visible) { setInnerVisible(true) progress.value = withTiming(1, { duration }) } else { progress.value = withTiming(0, { duration }, () => { runOnJS(setInnerVisible)(false) }) } }, [visible]) /* ================= Drawer 尺寸 ================= */ const drawerBox = useMemo(() => { switch (menuPosition) { case 'left': return { width: height, left: 0, top: 0, bottom: 0 } case 'right': return { width: height, right: 0, top: 0, bottom: 0 } case 'top': return { height, left: 0, right: 0, top: 0 } case 'bottom': default: return { height, left: 0, right: 0, bottom: 0 } } }, [menuPosition, height]) /* ================= 动画样式(关键) ================= */ const drawerStyle = useAnimatedStyle(() => { switch (menuPosition) { case 'bottom': { const y = interpolate(progress.value, [0, 1], [SCREEN.height, 0], 'clamp') return { transform: [{ translateY: y + drag.value }] } } case 'top': { const y = interpolate(progress.value, [0, 1], [-SCREEN.height, 0], 'clamp') return { transform: [{ translateY: y }] } } case 'left': { const x = interpolate(progress.value, [0, 1], [-SCREEN.width, 0], 'clamp') return { transform: [{ translateX: x }] } } case 'right': { const x = interpolate(progress.value, [0, 1], [SCREEN.width, 0], 'clamp') return { transform: [{ translateX: x }] } } default: return {} } }) const maskStyle = useAnimatedStyle(() => ({ opacity: interpolate(progress.value, [0, 1], [0, 0.45], 'clamp'), })) /* ================= 手势(只给 bottom) ================= */ const pan = Gesture.Pan() .onUpdate((e) => { if (menuPosition === 'bottom' && e.translationY > 0) { drag.value = e.translationY } }) .onEnd(() => { if (menuPosition === 'bottom' && drag.value > 120) { progress.value = withTiming(0, { duration }, () => { drag.value = 0 runOnJS(hideModal)() }) } else { drag.value = withTiming(0) } }) if (!innerVisible) return null return ( {/* 遮罩 */} isShadowHideModal && hideModal()} /> {/* Drawer */} {showHeader && ( header ?? ( {title} ) )} {children} ) } const styles = StyleSheet.create({ mask: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000', }, drawerBox: { position: 'absolute', }, drawer: { flex: 1, backgroundColor: '#fff', }, headTitle: { padding: 12, alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 18, fontWeight: '600', }, close: { position: 'absolute', right: 16, top: 12, }, })