1import React, {ReactNode, useEffect, useState} from 'react'; 2import { 3 LayoutRectangle, 4 StatusBar as OriginalStatusBar, 5 StyleProp, 6 StyleSheet, 7 TouchableWithoutFeedback, 8 View, 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'; 27import {IIconName} from '@/components/base/icon.tsx'; 28 29interface IAppBarProps { 30 titleTextOpacity?: number; 31 withStatusBar?: boolean; 32 color?: string; 33 actions?: Array<{ 34 icon: IIconName; 35 onPress?: () => void; 36 }>; 37 menu?: Array<{ 38 icon: IIconName; 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 onBackPress?: () => void; 49} 50 51const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp); 52const ANIMATION_DURATION = 500; 53 54const timingConfig = { 55 duration: ANIMATION_DURATION, 56 easing: ANIMATION_EASING, 57}; 58 59export default function AppBar(props: IAppBarProps) { 60 const { 61 titleTextOpacity = 1, 62 withStatusBar, 63 color: _color, 64 actions = [], 65 menu = [], 66 menuWithStatusBar = true, 67 containerStyle, 68 contentStyle, 69 children, 70 actionComponent, 71 onBackPress, 72 } = props; 73 74 const colors = useColors(); 75 const navigation = useNavigation(); 76 77 const bgColor = color(colors.appBar ?? colors.primary).toString(); 78 const contentColor = _color ?? colors.appBarText; 79 80 const [showMenu, setShowMenu] = useState(false); 81 const [menuIconLayout, setMenuIconLayout] = 82 useState<LayoutRectangle | null>(null); 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 onBackPress || 115 (() => { 116 navigation.goBack(); 117 }) 118 } 119 /> 120 <View style={[globalStyle.grow, styles.content, contentStyle]}> 121 {typeof children === 'string' ? ( 122 <ThemeText 123 fontSize="title" 124 fontWeight="bold" 125 numberOfLines={1} 126 color={ 127 titleTextOpacity !== 1 128 ? color(contentColor) 129 .alpha(titleTextOpacity) 130 .toString() 131 : contentColor 132 }> 133 {children} 134 </ThemeText> 135 ) : ( 136 children 137 )} 138 </View> 139 {actions.map((action, index) => ( 140 <IconButton 141 key={index} 142 name={action.icon} 143 sizeType="normal" 144 color={contentColor} 145 style={[globalStyle.notShrink, styles.rightButton]} 146 onPress={action.onPress} 147 /> 148 ))} 149 {actionComponent ?? null} 150 {menu?.length ? ( 151 <IconButton 152 name="ellipsis-vertical" 153 sizeType="normal" 154 onLayout={evt => { 155 setMenuIconLayout(evt.nativeEvent.layout); 156 }} 157 color={contentColor} 158 style={[globalStyle.notShrink, styles.rightButton]} 159 onPress={() => { 160 setShowMenu(true); 161 }} 162 /> 163 ) : null} 164 </View> 165 <Portal> 166 {showMenu ? ( 167 <TouchableWithoutFeedback 168 onPress={() => { 169 setShowMenu(false); 170 }}> 171 <View style={styles.blocker} /> 172 </TouchableWithoutFeedback> 173 ) : null} 174 <> 175 <Animated.View 176 pointerEvents={showMenu ? 'auto' : 'none'} 177 style={[ 178 { 179 borderBottomColor: colors.background, 180 left: 181 (menuIconLayout?.x ?? 0) + 182 (menuIconLayout?.width ?? 0) / 2 - 183 rpx(10), 184 top: 185 (menuIconLayout?.y ?? 0) + 186 (menuIconLayout?.height ?? 0) + 187 (menuWithStatusBar 188 ? OriginalStatusBar.currentHeight ?? 0 189 : 0), 190 }, 191 transformStyle, 192 styles.bubbleCorner, 193 ]} 194 /> 195 <Animated.View 196 pointerEvents={showMenu ? 'auto' : 'none'} 197 style={[ 198 { 199 backgroundColor: colors.background, 200 right: rpx(24), 201 top: 202 (menuIconLayout?.y ?? 0) + 203 (menuIconLayout?.height ?? 0) + 204 rpx(20) + 205 (menuWithStatusBar 206 ? OriginalStatusBar.currentHeight ?? 0 207 : 0), 208 shadowColor: colors.shadow, 209 }, 210 transformStyle, 211 styles.menu, 212 ]}> 213 {menu.map(it => 214 it.show !== false ? ( 215 <ListItem 216 key={it.title} 217 withHorizontalPadding 218 heightType="small" 219 onPress={() => { 220 setShowMenu(false); 221 // async 222 setTimeout(() => { 223 it.onPress?.(); 224 }, 20); 225 }}> 226 <ListItem.ListItemIcon icon={it.icon} /> 227 <ListItem.Content title={it.title} /> 228 </ListItem> 229 ) : null, 230 )} 231 </Animated.View> 232 </> 233 </Portal> 234 </> 235 ); 236} 237 238const styles = StyleSheet.create({ 239 container: { 240 width: '100%', 241 zIndex: 10000, 242 height: rpx(88), 243 flexDirection: 'row', 244 alignItems: 'center', 245 paddingHorizontal: rpx(24), 246 }, 247 content: { 248 flexDirection: 'row', 249 flexBasis: 0, 250 alignItems: 'center', 251 paddingHorizontal: rpx(24), 252 }, 253 rightButton: { 254 marginLeft: rpx(28), 255 }, 256 blocker: { 257 position: 'absolute', 258 bottom: 0, 259 left: 0, 260 width: '100%', 261 height: '100%', 262 zIndex: 10010, 263 }, 264 bubbleCorner: { 265 position: 'absolute', 266 borderColor: 'transparent', 267 borderWidth: rpx(10), 268 zIndex: 10012, 269 transformOrigin: 'right top', 270 opacity: 0, 271 }, 272 menu: { 273 width: rpx(340), 274 maxHeight: rpx(600), 275 borderRadius: rpx(8), 276 zIndex: 10011, 277 position: 'absolute', 278 opacity: 0, 279 shadowOffset: { 280 width: 0, 281 height: 2, 282 }, 283 shadowOpacity: 0.23, 284 shadowRadius: 2.62, 285 elevation: 4, 286 }, 287}); 288