1import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; 2import { 3 BackHandler, 4 DeviceEventEmitter, 5 EmitterSubscription, 6 Keyboard, 7 KeyboardAvoidingView, 8 NativeEventSubscription, 9 Pressable, 10 StyleSheet, 11} from 'react-native'; 12import rpx, {vh} from '@/utils/rpx'; 13 14import Animated, { 15 Easing, 16 runOnJS, 17 useAnimatedReaction, 18 useAnimatedStyle, 19 useSharedValue, 20 withTiming, 21} from 'react-native-reanimated'; 22import useColors from '@/hooks/useColors'; 23import {useSafeAreaInsets} from 'react-native-safe-area-context'; 24import useOrientation from '@/hooks/useOrientation'; 25import {panelInfoStore} from '../usePanel'; 26 27const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp); 28const ANIMATION_DURATION = 250; 29 30const timingConfig = { 31 duration: ANIMATION_DURATION, 32 easing: ANIMATION_EASING, 33}; 34 35interface IPanelBaseProps { 36 keyboardAvoidBehavior?: 'height' | 'padding' | 'position' | 'none'; 37 height?: number; 38 renderBody: (loading: boolean) => JSX.Element; 39 awareKeyboard?: boolean; 40} 41 42export default function (props: IPanelBaseProps) { 43 const { 44 height = vh(60), 45 renderBody, 46 keyboardAvoidBehavior, 47 awareKeyboard, 48 } = props; 49 const snapPoint = useSharedValue(0); 50 51 const colors = useColors(); 52 const [loading, setLoading] = useState(true); // 是否处于弹出状态 53 const timerRef = useRef<any>(); 54 const safeAreaInsets = useSafeAreaInsets(); 55 const orientation = useOrientation(); 56 const useAnimatedBase = useMemo( 57 () => (orientation === 'horizonal' ? rpx(750) : height), 58 [orientation], 59 ); 60 const backHandlerRef = useRef<NativeEventSubscription>(); 61 62 const hideCallbackRef = useRef<Function[]>([]); 63 64 const [keyboardHeight, setKeyboardHeight] = useState(0); 65 useEffect(() => { 66 snapPoint.value = withTiming(1, timingConfig); 67 timerRef.current = setTimeout(() => { 68 if (loading) { 69 // 兜底 70 setLoading(false); 71 } 72 }, 400); 73 if (backHandlerRef.current) { 74 backHandlerRef.current?.remove(); 75 backHandlerRef.current = undefined; 76 } 77 backHandlerRef.current = BackHandler.addEventListener( 78 'hardwareBackPress', 79 () => { 80 snapPoint.value = withTiming(0, timingConfig); 81 return true; 82 }, 83 ); 84 85 const listenerSubscription = DeviceEventEmitter.addListener( 86 'hidePanel', 87 (callback?: () => void) => { 88 if (callback) { 89 hideCallbackRef.current.push(callback); 90 } 91 snapPoint.value = withTiming(0, timingConfig); 92 }, 93 ); 94 95 let keyboardDidShowListener: EmitterSubscription; 96 let keyboardDidHideListener: EmitterSubscription; 97 if (awareKeyboard) { 98 Keyboard.addListener('keyboardDidChangeFrame', event => { 99 console.log(event, 'KKss'); 100 }); 101 Keyboard.addListener('keyboardWillShow', event => { 102 console.log(event, 'KKsss'); 103 }); 104 keyboardDidShowListener = Keyboard.addListener( 105 'keyboardDidShow', 106 event => { 107 setKeyboardHeight(event.endCoordinates.height); 108 }, 109 ); 110 111 keyboardDidHideListener = Keyboard.addListener( 112 'keyboardDidHide', 113 () => { 114 setKeyboardHeight(0); 115 }, 116 ); 117 } 118 119 return () => { 120 if (timerRef.current) { 121 clearTimeout(timerRef.current); 122 timerRef.current = null; 123 } 124 if (backHandlerRef.current) { 125 backHandlerRef.current?.remove(); 126 backHandlerRef.current = undefined; 127 } 128 listenerSubscription.remove(); 129 keyboardDidShowListener?.remove(); 130 keyboardDidHideListener?.remove(); 131 }; 132 }, []); 133 134 const maskAnimated = useAnimatedStyle(() => { 135 return { 136 opacity: snapPoint.value * 0.5, 137 }; 138 }); 139 140 const panelAnimated = useAnimatedStyle(() => { 141 return { 142 transform: [ 143 orientation === 'vertical' 144 ? { 145 translateY: (1 - snapPoint.value) * useAnimatedBase, 146 } 147 : { 148 translateX: (1 - snapPoint.value) * useAnimatedBase, 149 }, 150 ], 151 }; 152 }, [orientation]); 153 154 const mountPanel = useCallback(() => { 155 setLoading(false); 156 }, []); 157 158 const unmountPanel = useCallback(() => { 159 panelInfoStore.setValue({ 160 name: null, 161 payload: null, 162 }); 163 hideCallbackRef.current.forEach(cb => cb?.()); 164 }, []); 165 166 useAnimatedReaction( 167 () => snapPoint.value, 168 (result, prevResult) => { 169 if (prevResult && result > prevResult && result > 0.8) { 170 runOnJS(mountPanel)(); 171 } 172 if (prevResult && result < prevResult && result === 0) { 173 runOnJS(unmountPanel)(); 174 } 175 }, 176 [], 177 ); 178 179 const panelBody = ( 180 <Animated.View 181 style={[ 182 style.wrapper, 183 { 184 backgroundColor: colors.backdrop, 185 height: 186 orientation === 'horizonal' 187 ? vh(100) - safeAreaInsets.top 188 : height - keyboardHeight, 189 }, 190 panelAnimated, 191 ]}> 192 {renderBody(loading)} 193 </Animated.View> 194 ); 195 196 return ( 197 <> 198 <Pressable 199 style={style.maskWrapper} 200 onPress={() => { 201 snapPoint.value = withTiming(0, timingConfig); 202 }}> 203 <Animated.View 204 style={[style.maskWrapper, style.mask, maskAnimated]} 205 /> 206 </Pressable> 207 {keyboardAvoidBehavior === 'none' ? ( 208 panelBody 209 ) : ( 210 <KeyboardAvoidingView 211 style={style.kbContainer} 212 behavior={keyboardAvoidBehavior || 'position'}> 213 {panelBody} 214 </KeyboardAvoidingView> 215 )} 216 </> 217 ); 218} 219 220const style = StyleSheet.create({ 221 maskWrapper: { 222 position: 'absolute', 223 width: '100%', 224 height: '100%', 225 top: 0, 226 left: 0, 227 right: 0, 228 bottom: 0, 229 zIndex: 15000, 230 }, 231 mask: { 232 backgroundColor: '#000', 233 opacity: 0.5, 234 }, 235 wrapper: { 236 position: 'absolute', 237 width: rpx(750), 238 bottom: 0, 239 right: 0, 240 borderTopLeftRadius: rpx(28), 241 borderTopRightRadius: rpx(28), 242 zIndex: 15010, 243 }, 244 kbContainer: { 245 zIndex: 15010, 246 }, 247}); 248