u
This commit is contained in:
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,
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user