1import React, {useCallback, useEffect, useMemo, useRef} from 'react'; 2import { 3 BackHandler, 4 DeviceEventEmitter, 5 NativeEventSubscription, 6 Pressable, 7 StyleSheet, 8 ViewStyle, 9} from 'react-native'; 10 11import Animated, { 12 Easing, 13 EasingFunction, 14 runOnJS, 15 useAnimatedReaction, 16 useAnimatedStyle, 17 useSharedValue, 18 withTiming, 19} from 'react-native-reanimated'; 20import useColors from '@/hooks/useColors'; 21import {panelInfoStore} from '../usePanel'; 22import {vh} from '@/utils/rpx.ts'; 23import useOrientation from '@/hooks/useOrientation.ts'; 24 25const ANIMATION_EASING: EasingFunction = Easing.out(Easing.exp); 26const ANIMATION_DURATION = 250; 27 28const timingConfig = { 29 duration: ANIMATION_DURATION, 30 easing: ANIMATION_EASING, 31}; 32 33interface IPanelFullScreenProps { 34 // 有遮罩 35 hasMask?: boolean; 36 // 内容 37 children?: React.ReactNode; 38 // 内容区样式 39 containerStyle?: ViewStyle; 40 41 animationType?: 'SlideToTop' | 'Scale'; 42} 43 44export default function (props: IPanelFullScreenProps) { 45 const { 46 hasMask, 47 containerStyle, 48 children, 49 animationType = 'SlideToTop', 50 } = props; 51 const snapPoint = useSharedValue(0); 52 53 const colors = useColors(); 54 55 const backHandlerRef = useRef<NativeEventSubscription>(); 56 57 const hideCallbackRef = useRef<Function[]>([]); 58 59 const orientation = useOrientation(); 60 const windowHeight = useMemo(() => vh(100), [orientation]); 61 62 useEffect(() => { 63 snapPoint.value = withTiming(1, timingConfig); 64 65 if (backHandlerRef.current) { 66 backHandlerRef.current?.remove(); 67 backHandlerRef.current = undefined; 68 } 69 backHandlerRef.current = BackHandler.addEventListener( 70 'hardwareBackPress', 71 () => { 72 snapPoint.value = withTiming(0, timingConfig); 73 return true; 74 }, 75 ); 76 77 const listenerSubscription = DeviceEventEmitter.addListener( 78 'hidePanel', 79 (callback?: () => void) => { 80 if (callback) { 81 hideCallbackRef.current.push(callback); 82 } 83 snapPoint.value = withTiming(0, timingConfig); 84 }, 85 ); 86 87 return () => { 88 if (backHandlerRef.current) { 89 backHandlerRef.current?.remove(); 90 backHandlerRef.current = undefined; 91 } 92 listenerSubscription.remove(); 93 }; 94 }, []); 95 96 const maskAnimated = useAnimatedStyle(() => { 97 return { 98 opacity: withTiming(snapPoint.value * 0.5, timingConfig), 99 }; 100 }); 101 102 const panelAnimated = useAnimatedStyle(() => { 103 if (animationType === 'SlideToTop') { 104 return { 105 transform: [ 106 { 107 translateY: withTiming( 108 (1 - snapPoint.value) * windowHeight, 109 timingConfig, 110 ), 111 }, 112 ], 113 }; 114 } else { 115 return { 116 transform: [ 117 { 118 scale: 0.3 + snapPoint.value * 0.7, 119 }, 120 ], 121 opacity: snapPoint.value, 122 }; 123 } 124 }); 125 126 const unmountPanel = useCallback(() => { 127 panelInfoStore.setValue({ 128 name: null, 129 payload: null, 130 }); 131 hideCallbackRef.current.forEach(cb => cb?.()); 132 }, []); 133 134 useAnimatedReaction( 135 () => snapPoint.value, 136 (result, prevResult) => { 137 if (prevResult && result < prevResult && result === 0) { 138 runOnJS(unmountPanel)(); 139 } 140 }, 141 [], 142 ); 143 return ( 144 <> 145 {hasMask ? ( 146 <Pressable 147 style={style.maskWrapper} 148 onPress={() => { 149 snapPoint.value = withTiming(0, timingConfig); 150 }}> 151 <Animated.View 152 style={[style.maskWrapper, style.mask, maskAnimated]} 153 /> 154 </Pressable> 155 ) : null} 156 <Animated.View 157 pointerEvents={hasMask ? 'box-none' : undefined} 158 style={[ 159 style.wrapper, 160 !hasMask 161 ? { 162 backgroundColor: colors.background, 163 } 164 : null, 165 panelAnimated, 166 containerStyle, 167 ]}> 168 {children} 169 </Animated.View> 170 </> 171 ); 172} 173 174const style = StyleSheet.create({ 175 maskWrapper: { 176 position: 'absolute', 177 width: '100%', 178 height: '100%', 179 top: 0, 180 left: 0, 181 right: 0, 182 bottom: 0, 183 zIndex: 15000, 184 }, 185 mask: { 186 backgroundColor: '#000', 187 opacity: 0.5, 188 }, 189 wrapper: { 190 position: 'absolute', 191 width: '100%', 192 height: '100%', 193 bottom: 0, 194 right: 0, 195 zIndex: 15010, 196 flexDirection: 'column', 197 }, 198 kbContainer: { 199 zIndex: 15010, 200 }, 201}); 202