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