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.appBarText; 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, index) => ( 137 <IconButton 138 key={index} 139 name={action.icon} 140 sizeType="normal" 141 color={contentColor} 142 style={[globalStyle.notShrink, styles.rightButton]} 143 onPress={action.onPress} 144 /> 145 ))} 146 {actionComponent ?? null} 147 {menu?.length ? ( 148 <IconButton 149 name="dots-vertical" 150 sizeType="normal" 151 onLayout={e => { 152 menuIconLayoutRef.current = e.nativeEvent.layout; 153 }} 154 color={contentColor} 155 style={[globalStyle.notShrink, styles.rightButton]} 156 onPress={() => { 157 setShowMenu(true); 158 }} 159 /> 160 ) : null} 161 </View> 162 <Portal> 163 {showMenu ? ( 164 <TouchableWithoutFeedback 165 onPress={() => { 166 setShowMenu(false); 167 }}> 168 <View style={styles.blocker} /> 169 </TouchableWithoutFeedback> 170 ) : null} 171 <> 172 <Animated.View 173 pointerEvents={showMenu ? 'auto' : 'none'} 174 style={[ 175 { 176 borderBottomColor: colors.background, 177 left: 178 (menuIconLayoutRef.current?.x ?? 0) + 179 (menuIconLayoutRef.current?.width ?? 0) / 180 2 - 181 rpx(10), 182 top: 183 (menuIconLayoutRef.current?.y ?? 0) + 184 (menuIconLayoutRef.current?.height ?? 0) + 185 (menuWithStatusBar 186 ? OriginalStatusBar.currentHeight ?? 0 187 : 0), 188 }, 189 transformStyle, 190 styles.bubbleCorner, 191 ]} 192 /> 193 <Animated.View 194 pointerEvents={showMenu ? 'auto' : 'none'} 195 style={[ 196 { 197 backgroundColor: colors.background, 198 right: rpx(24), 199 top: 200 (menuIconLayoutRef.current?.y ?? 0) + 201 (menuIconLayoutRef.current?.height ?? 0) + 202 rpx(20) + 203 (menuWithStatusBar 204 ? OriginalStatusBar.currentHeight ?? 0 205 : 0), 206 shadowColor: colors.shadow, 207 }, 208 transformStyle, 209 styles.menu, 210 ]}> 211 {menu.map(it => 212 it.show !== false ? ( 213 <ListItem 214 key={it.title} 215 withHorizonalPadding 216 heightType="small" 217 onPress={() => { 218 it.onPress?.(); 219 setShowMenu(false); 220 }}> 221 <ListItem.ListItemIcon icon={it.icon} /> 222 <ListItem.Content title={it.title} /> 223 </ListItem> 224 ) : null, 225 )} 226 </Animated.View> 227 </> 228 </Portal> 229 </> 230 ); 231} 232 233const styles = StyleSheet.create({ 234 container: { 235 width: '100%', 236 zIndex: 10000, 237 height: rpx(88), 238 flexDirection: 'row', 239 paddingHorizontal: rpx(24), 240 }, 241 content: { 242 flexDirection: 'row', 243 flexBasis: 0, 244 alignItems: 'center', 245 paddingHorizontal: rpx(24), 246 }, 247 rightButton: { 248 marginLeft: rpx(28), 249 }, 250 blocker: { 251 position: 'absolute', 252 bottom: 0, 253 left: 0, 254 width: '100%', 255 height: '100%', 256 zIndex: 10010, 257 }, 258 bubbleCorner: { 259 position: 'absolute', 260 borderColor: 'transparent', 261 borderWidth: rpx(10), 262 zIndex: 10012, 263 transformOrigin: 'right top', 264 opacity: 0, 265 }, 266 menu: { 267 width: rpx(340), 268 maxHeight: rpx(600), 269 borderRadius: rpx(8), 270 zIndex: 10011, 271 position: 'absolute', 272 opacity: 0, 273 shadowOffset: { 274 width: 0, 275 height: 2, 276 }, 277 shadowOpacity: 0.23, 278 shadowRadius: 2.62, 279 elevation: 4, 280 }, 281}); 282