xref: /MusicFree/src/components/panels/base/panelBase.tsx (revision 1795823514ebd83a3e0f24980c9076bd96e6d14e)
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