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