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