xref: /MusicFree/src/components/base/appBar.tsx (revision e650bfb34226e2a09d15cbf7832c4805a87cd60e)
1import React, {ReactNode, useEffect, useRef, useState} from 'react';
2import {
3    LayoutRectangle,
4    StyleSheet,
5    TouchableWithoutFeedback,
6    View,
7    StatusBar as OriginalStatusBar,
8} from 'react-native';
9import rpx from '@/utils/rpx';
10import useColors from '@/hooks/useColors';
11import StatusBar from './statusBar';
12import color from 'color';
13import IconButton from './iconButton';
14import globalStyle from '@/constants/globalStyle';
15import ThemeText from './themeText';
16import {useNavigation} from '@react-navigation/native';
17import Animated, {
18    Easing,
19    useAnimatedStyle,
20    useSharedValue,
21    withTiming,
22} from 'react-native-reanimated';
23import Portal from './portal';
24import IconTextButton from './iconTextButton';
25
26interface IAppBarProps {
27    backgroundOpacity?: number;
28    titleTextOpacity?: number;
29    withStatusBar?: boolean;
30    color?: string;
31    actions?: Array<{
32        icon: string;
33        onPress?: () => void;
34    }>;
35    menu?: Array<{
36        icon: string;
37        title: string;
38        onPress?: () => void;
39    }>;
40    menuWithStatusBar?: boolean;
41    children?: string | ReactNode;
42}
43
44const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp);
45const ANIMATION_DURATION = 500;
46
47const timingConfig = {
48    duration: ANIMATION_DURATION,
49    easing: ANIMATION_EASING,
50};
51
52export default function AppBar(props: IAppBarProps) {
53    const {
54        backgroundOpacity = 1,
55        titleTextOpacity = 1,
56        withStatusBar,
57        color: _color,
58        actions = [],
59        menu = [],
60        menuWithStatusBar = true,
61        children,
62    } = props;
63
64    const colors = useColors();
65    const navigation = useNavigation();
66
67    const bgColor = color(colors.primary).alpha(backgroundOpacity).toString();
68    const contentColor = _color ?? colors.headerText;
69
70    const [showMenu, setShowMenu] = useState(false);
71    const menuIconLayoutRef = useRef<LayoutRectangle>();
72    const scaleRate = useSharedValue(0);
73
74    useEffect(() => {
75        if (showMenu) {
76            scaleRate.value = withTiming(1, timingConfig);
77        } else {
78            scaleRate.value = withTiming(0, timingConfig);
79        }
80    }, [showMenu]);
81
82    const transformStyle = useAnimatedStyle(() => {
83        return {
84            opacity: scaleRate.value,
85        };
86    });
87
88    return (
89        <>
90            {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null}
91            <View style={[styles.container, {backgroundColor: bgColor}]}>
92                <IconButton
93                    name="arrow-left"
94                    sizeType="normal"
95                    color={contentColor}
96                    style={globalStyle.notShrink}
97                    onPress={() => {
98                        navigation.goBack();
99                    }}
100                />
101                <View style={[globalStyle.grow, styles.content]}>
102                    {typeof children === 'string' ? (
103                        <ThemeText
104                            fontSize="title"
105                            fontWeight="bold"
106                            numberOfLines={1}
107                            color={
108                                titleTextOpacity !== 1
109                                    ? color(contentColor)
110                                          .alpha(titleTextOpacity)
111                                          .toString()
112                                    : contentColor
113                            }>
114                            {children}
115                        </ThemeText>
116                    ) : (
117                        children
118                    )}
119                </View>
120                {actions.map(action => (
121                    <IconButton
122                        name={action.icon}
123                        sizeType="normal"
124                        color={contentColor}
125                        style={[globalStyle.notShrink, styles.rightButton]}
126                        onPress={action.onPress}
127                    />
128                ))}
129                {menu?.length ? (
130                    <IconButton
131                        name="dots-vertical"
132                        sizeType="normal"
133                        onLayout={e => {
134                            menuIconLayoutRef.current = e.nativeEvent.layout;
135                        }}
136                        color={contentColor}
137                        style={[globalStyle.notShrink, styles.rightButton]}
138                        onPress={() => {
139                            setShowMenu(true);
140                        }}
141                    />
142                ) : null}
143            </View>
144            <Portal>
145                {showMenu ? (
146                    <TouchableWithoutFeedback
147                        onPress={() => {
148                            setShowMenu(false);
149                        }}>
150                        <View style={styles.blocker} />
151                    </TouchableWithoutFeedback>
152                ) : null}
153                <>
154                    <Animated.View
155                        pointerEvents={showMenu ? 'auto' : 'none'}
156                        style={[
157                            {
158                                borderBottomColor: colors.backdrop,
159                                left:
160                                    (menuIconLayoutRef.current?.x ?? 0) +
161                                    (menuIconLayoutRef.current?.width ?? 0) /
162                                        2 -
163                                    rpx(10),
164                                top:
165                                    (menuIconLayoutRef.current?.y ?? 0) +
166                                    (menuIconLayoutRef.current?.height ?? 0) +
167                                    (menuWithStatusBar
168                                        ? OriginalStatusBar.currentHeight ?? 0
169                                        : 0),
170                            },
171                            transformStyle,
172                            styles.bubbleCorner,
173                        ]}
174                    />
175                    <Animated.View
176                        pointerEvents={showMenu ? 'auto' : 'none'}
177                        style={[
178                            {
179                                backgroundColor: colors.headerText,
180                                right: rpx(24),
181                                top:
182                                    (menuIconLayoutRef.current?.y ?? 0) +
183                                    (menuIconLayoutRef.current?.height ?? 0) +
184                                    rpx(20) +
185                                    (menuWithStatusBar
186                                        ? OriginalStatusBar.currentHeight ?? 0
187                                        : 0),
188                            },
189                            transformStyle,
190                            styles.menu,
191                        ]}>
192                        {menu.map(it => (
193                            <IconTextButton
194                                icon={it.icon}
195                                onPress={() => {
196                                    it.onPress?.();
197                                    setShowMenu(false);
198                                }}>
199                                {it.title}
200                            </IconTextButton>
201                        ))}
202                    </Animated.View>
203                </>
204            </Portal>
205        </>
206    );
207}
208
209const styles = StyleSheet.create({
210    container: {
211        width: '100%',
212        zIndex: 10000,
213        height: rpx(88),
214        flexDirection: 'row',
215        paddingHorizontal: rpx(24),
216    },
217    content: {
218        flexDirection: 'row',
219        flexBasis: 0,
220        alignItems: 'center',
221        paddingHorizontal: rpx(24),
222    },
223    rightButton: {
224        marginLeft: rpx(28),
225    },
226    blocker: {
227        position: 'absolute',
228        bottom: 0,
229        left: 0,
230        width: '100%',
231        height: '100%',
232        zIndex: 10010,
233    },
234    bubbleCorner: {
235        position: 'absolute',
236        borderColor: 'transparent',
237        borderWidth: rpx(10),
238        zIndex: 10012,
239        transformOrigin: 'right top',
240        opacity: 0,
241    },
242    menu: {
243        width: rpx(340),
244        maxHeight: rpx(600),
245        borderRadius: rpx(8),
246        zIndex: 10011,
247        position: 'absolute',
248        opacity: 0,
249        shadowColor: '#000',
250        shadowOffset: {
251            width: 0,
252            height: 2,
253        },
254        shadowOpacity: 0.23,
255        shadowRadius: 2.62,
256        elevation: 4,
257    },
258});
259