xref: /MusicFree/src/components/panels/base/panelBase.tsx (revision 734113be9d256a2b4d36bb272d6d3565beaeb236)
1import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2import {
3    BackHandler,
4    DeviceEventEmitter,
5    NativeEventSubscription,
6    Pressable,
7    StyleSheet,
8} from 'react-native';
9import rpx, {vh} from '@/utils/rpx';
10
11import Animated, {
12    Easing,
13    runOnJS,
14    useAnimatedReaction,
15    useAnimatedStyle,
16    useSharedValue,
17    withTiming,
18} from 'react-native-reanimated';
19import useColors from '@/hooks/useColors';
20import {useSafeAreaInsets} from 'react-native-safe-area-context';
21import useOrientation from '@/hooks/useOrientation';
22import {panelInfoStore} from '../usePanel';
23
24const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp);
25const ANIMATION_DURATION = 250;
26
27const timingConfig = {
28    duration: ANIMATION_DURATION,
29    easing: ANIMATION_EASING,
30};
31
32interface IPanelBaseProps {
33    height?: number;
34    renderBody: (loading: boolean) => JSX.Element;
35}
36
37export default function (props: IPanelBaseProps) {
38    const {height = vh(60), renderBody} = props;
39    const snapPoint = useSharedValue(0);
40
41    const colors = useColors();
42    const [loading, setLoading] = useState(true); // 是否处于弹出状态
43    const timerRef = useRef<any>();
44    const safeAreaInsets = useSafeAreaInsets();
45    const orientation = useOrientation();
46    const useAnimatedBase = useMemo(
47        () => (orientation === 'horizonal' ? rpx(750) : height),
48        [orientation],
49    );
50    const backHandlerRef = useRef<NativeEventSubscription>();
51
52    const hideCallbackRef = useRef<Function[]>([]);
53
54    useEffect(() => {
55        snapPoint.value = withTiming(1, timingConfig);
56        timerRef.current = setTimeout(() => {
57            if (loading) {
58                // 兜底
59                setLoading(false);
60            }
61        }, 400);
62        if (backHandlerRef.current) {
63            backHandlerRef.current?.remove();
64            backHandlerRef.current = undefined;
65        }
66        backHandlerRef.current = BackHandler.addEventListener(
67            'hardwareBackPress',
68            () => {
69                snapPoint.value = withTiming(0, timingConfig);
70                return true;
71            },
72        );
73
74        const listenerSubscription = DeviceEventEmitter.addListener(
75            'hidePanel',
76            (callback?: () => void) => {
77                if (callback) {
78                    hideCallbackRef.current.push(callback);
79                }
80                snapPoint.value = withTiming(0, timingConfig);
81            },
82        );
83
84        return () => {
85            if (timerRef.current) {
86                clearTimeout(timerRef.current);
87                timerRef.current = null;
88            }
89            if (backHandlerRef.current) {
90                backHandlerRef.current?.remove();
91                backHandlerRef.current = undefined;
92            }
93            listenerSubscription.remove();
94        };
95    }, []);
96
97    const maskAnimated = useAnimatedStyle(() => {
98        return {
99            opacity: withTiming(snapPoint.value * 0.5, timingConfig),
100        };
101    });
102
103    const panelAnimated = useAnimatedStyle(() => {
104        return {
105            transform: [
106                orientation === 'vertical'
107                    ? {
108                          translateY: withTiming(
109                              (1 - snapPoint.value) * useAnimatedBase,
110                              timingConfig,
111                          ),
112                      }
113                    : {
114                          translateX: withTiming(
115                              (1 - snapPoint.value) * useAnimatedBase,
116                              timingConfig,
117                          ),
118                      },
119            ],
120        };
121    }, [orientation]);
122
123    const mountPanel = useCallback(() => {
124        setLoading(false);
125    }, []);
126
127    const unmountPanel = useCallback(() => {
128        panelInfoStore.setValue({
129            name: null,
130            payload: null,
131        });
132        hideCallbackRef.current.forEach(cb => cb?.());
133    }, []);
134
135    useAnimatedReaction(
136        () => snapPoint.value,
137        (result, prevResult) => {
138            if (prevResult && result > prevResult && result > 0.8) {
139                runOnJS(mountPanel)();
140            }
141            if (prevResult && result < prevResult && result === 0) {
142                runOnJS(unmountPanel)();
143            }
144        },
145        [],
146    );
147
148    return (
149        <>
150            <Pressable
151                style={style.maskWrapper}
152                onPress={() => {
153                    snapPoint.value = withTiming(0, timingConfig);
154                }}>
155                <Animated.View
156                    style={[style.maskWrapper, style.mask, maskAnimated]}
157                />
158            </Pressable>
159            <Animated.View
160                style={[
161                    style.wrapper,
162                    {
163                        backgroundColor: colors.primary,
164                        height:
165                            orientation === 'horizonal'
166                                ? vh(100) - safeAreaInsets.top
167                                : height,
168                    },
169                    panelAnimated,
170                ]}>
171                {renderBody(loading)}
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    },
187    mask: {
188        backgroundColor: '#333333',
189        opacity: 0.5,
190    },
191    wrapper: {
192        position: 'absolute',
193        width: rpx(750),
194        bottom: 0,
195        right: 0,
196        borderTopLeftRadius: rpx(28),
197        borderTopRightRadius: rpx(28),
198    },
199});
200