This commit is contained in:
oneao committed 2026-01-26 22:31:12 +08:00
1 parent 5a79414319
commit 8663cb2e3d
28 files changed
+744 -756

No files matched your search

@@ -1,19 +1,18 @@
import type { ReactNode } from 'react'
import type { DimensionValue, ViewStyle } from 'react-native'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
Animated,
Dimensions,
Modal,
Platform,
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
} 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 WINDOW = Dimensions.get('window')
const SCREEN = Dimensions.get('screen')
interface DrawerMenuProps {
visible: boolean
@@ -27,218 +26,145 @@ interface DrawerMenuProps {
CircleNum?: number
isShadowHideModal?: boolean
title?: string
showHeader?: boolean
}
export default function Drawer({
visible,
height,
height = 300,
hideModal,
duration = 300,
isCircle,
menuPosition = 'bottom',
children,
CircleNum = 8,
CircleNum = 12,
isShadowHideModal = true,
header,
title,
showHeader = true,
}: DrawerMenuProps) {
const [menuPositionStyle, setMenuPositionStyle] = useState<ViewStyle>()
const [drawerMenuBox, setDrawerMenuBox] = useState<ViewStyle>()
const progress = useSharedValue(0)
const drag = useSharedValue(0)
const [innerVisible, setInnerVisible] = useState(false)
const anim = useRef(new Animated.Value(0)).current
/* ================= 显隐 ================= */
useEffect(() => {
if (visible) {
setInnerVisible(true)
open()
progress.value = withTiming(1, { duration })
}
else {
close()
progress.value = withTiming(0, { duration }, () => {
runOnJS(setInnerVisible)(false)
})
}
}, [visible])
useEffect(() => {
/* ================= Drawer 尺寸 ================= */
const drawerBox = useMemo<ViewStyle>(() => {
switch (menuPosition) {
case 'left':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopRightRadius: isCircle ? CircleNum : 0,
borderBottomRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: height,
height: WINDOW.height,
position: 'absolute',
left: 0,
top: 0,
})
break
return { width: height, left: 0, top: 0, bottom: 0 }
case 'right':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderBottomLeftRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: height,
height: WINDOW.height,
position: 'absolute',
right: 0,
top: 0,
})
break
return { width: height, right: 0, top: 0, bottom: 0 }
case 'top':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderBottomLeftRadius: isCircle ? CircleNum : 0,
borderBottomRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: WINDOW.width,
height,
position: 'absolute',
top: 0,
right: 0,
})
break
return { height, left: 0, right: 0, top: 0 }
case 'bottom':
default:
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderTopRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: WINDOW.width,
height,
position: 'absolute',
bottom: 0,
right: 0,
})
break
return { height, left: 0, right: 0, bottom: 0 }
}
}, [menuPosition, height, isCircle, CircleNum])
}, [menuPosition, height])
/* ================== 动画 ================== */
const open = () => {
Animated.timing(anim, {
toValue: 1,
duration,
useNativeDriver: true,
}).start()
}
/* ================= 动画样式(关键) ================= */
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 close = () => {
Animated.timing(anim, {
toValue: 0,
duration,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
setInnerVisible(false)
hideModal()
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)
}
})
}
/* ================== 位移 ================== */
const getTransform = useCallback(() => {
switch (menuPosition) {
case 'left':
return {
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [-WINDOW.width, 0],
}),
}
case 'right':
return {
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [WINDOW.width, 0],
}),
}
case 'top':
return {
translateY: anim.interpolate({
inputRange: [0, 1],
outputRange: [-WINDOW.height, 0],
}),
}
case 'bottom':
default:
return {
translateY: anim.interpolate({
inputRange: [0, 1],
outputRange: [WINDOW.height, 0],
}),
}
}
}, [menuPosition])
if (!innerVisible)
return null
return (
<Modal transparent visible animationType="none">
<View style={StyleSheet.absoluteFill} pointerEvents="box-none" className="z-[99]">
{/* 遮罩 */}
<TouchableWithoutFeedback onPress={() => isShadowHideModal && close()}>
<Animated.View
style={[
styles.mask,
{
opacity: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.5],
}),
},
]}
/>
</TouchableWithoutFeedback>
<Animated.View
style={[styles.mask, maskStyle]}
onTouchEnd={() => isShadowHideModal && hideModal()}
/>
{/* Drawer */}
<View style={drawerMenuBox} pointerEvents="box-none">
<Animated.View
pointerEvents="box-none"
style={[
styles.container,
{
transform: [getTransform()],
},
]}
>
<View style={menuPositionStyle}>
{menuPosition !== 'bottom' && (
<View style={{ paddingTop: Platform.OS === 'ios' ? 50 : 0 }} />
)}
{
header || (
<View style={[styles.drawerBox, drawerBox]}>
<GestureDetector gesture={pan}>
<Animated.View
style={[
styles.drawer,
drawerStyle,
{
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderTopRightRadius: isCircle ? CircleNum : 0,
},
]}
>
{showHeader && (
header ?? (
<View style={styles.headTitle}>
<Text style={styles.title}>{title}</Text>
<LucideIcon
icon="X"
size={22}
color="black"
style={{ position: 'absolute', right: 15, top: 11 }}
onPress={close}
style={styles.close}
onPress={hideModal}
/>
</View>
)
}
)}
{children}
</View>
</Animated.View>
</Animated.View>
</GestureDetector>
</View>
</Modal>
</View>
)
}
@@ -247,18 +173,25 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
backgroundColor: '#000',
},
container: {
height: '100%',
width: '100%',
drawerBox: {
position: 'absolute',
},
drawer: {
flex: 1,
backgroundColor: '#fff',
},
headTitle: {
width: '100%',
padding: 10,
justifyContent: 'center',
padding: 12,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
},
close: {
position: 'absolute',
right: 16,
top: 12,
},
})