xref: /MusicFree/src/components/panels/base/panelFullscreen.tsx (revision 4da0658b1bf77de935d6a60ad1a26e4b92d13606)
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