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 = 1; 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 = 0; 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 = 0; 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: withTiming( 119 0.3 + snapPoint.value * 0.7, 120 timingConfig, 121 ), 122 }, 123 ], 124 opacity: withTiming(snapPoint.value, timingConfig), 125 }; 126 } 127 }); 128 129 const unmountPanel = useCallback(() => { 130 panelInfoStore.setValue({ 131 name: null, 132 payload: null, 133 }); 134 hideCallbackRef.current.forEach(cb => cb?.()); 135 }, []); 136 137 useAnimatedReaction( 138 () => snapPoint.value, 139 (result, prevResult) => { 140 if (prevResult && result < prevResult && result === 0) { 141 runOnJS(unmountPanel)(); 142 } 143 }, 144 [], 145 ); 146 return ( 147 <> 148 {hasMask ? ( 149 <Pressable 150 style={style.maskWrapper} 151 onPress={() => { 152 snapPoint.value = withTiming(0, timingConfig); 153 }}> 154 <Animated.View 155 style={[style.maskWrapper, style.mask, maskAnimated]} 156 /> 157 </Pressable> 158 ) : null} 159 <Animated.View 160 pointerEvents={hasMask ? 'box-none' : undefined} 161 style={[ 162 style.wrapper, 163 !hasMask 164 ? { 165 backgroundColor: colors.background, 166 } 167 : null, 168 panelAnimated, 169 containerStyle, 170 ]}> 171 {children} 172 </Animated.View> 173 </> 174 ); 175} 176 177const style = StyleSheet.create({ 178 maskWrapper: { 179 position: 'absolute', 180 width: '100%', 181 height: '100%', 182 top: 0, 183 left: 0, 184 right: 0, 185 bottom: 0, 186 zIndex: 15000, 187 }, 188 mask: { 189 backgroundColor: '#000', 190 opacity: 0.5, 191 }, 192 wrapper: { 193 position: 'absolute', 194 width: '100%', 195 height: '100%', 196 bottom: 0, 197 right: 0, 198 zIndex: 15010, 199 flexDirection: 'column', 200 }, 201 kbContainer: { 202 zIndex: 15010, 203 }, 204}); 205