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} 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 titleTextOpacity = 1, 61 withStatusBar, 62 color: _color, 63 actions = [], 64 menu = [], 65 menuWithStatusBar = true, 66 containerStyle, 67 contentStyle, 68 children, 69 actionComponent, 70 } = props; 71 72 const colors = useColors(); 73 const navigation = useNavigation(); 74 75 const bgColor = color(colors.appBar ?? colors.primary).toString(); 76 const contentColor = _color ?? colors.appBarText; 77 78 const [showMenu, setShowMenu] = useState(false); 79 const [menuIconLayout, setMenuIconLayout] = 80 useState<LayoutRectangle | null>(null); 81 const scaleRate = useSharedValue(0); 82 83 useEffect(() => { 84 if (showMenu) { 85 scaleRate.value = withTiming(1, timingConfig); 86 } else { 87 scaleRate.value = withTiming(0, timingConfig); 88 } 89 }, [showMenu]); 90 91 const transformStyle = useAnimatedStyle(() => { 92 return { 93 opacity: scaleRate.value, 94 }; 95 }); 96 97 return ( 98 <> 99 {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null} 100 <View 101 style={[ 102 styles.container, 103 containerStyle, 104 {backgroundColor: bgColor}, 105 ]}> 106 <IconButton 107 name="arrow-left" 108 sizeType="normal" 109 color={contentColor} 110 style={globalStyle.notShrink} 111 onPress={() => { 112 navigation.goBack(); 113 }} 114 /> 115 <View style={[globalStyle.grow, styles.content, contentStyle]}> 116 {typeof children === 'string' ? ( 117 <ThemeText 118 fontSize="title" 119 fontWeight="bold" 120 numberOfLines={1} 121 color={ 122 titleTextOpacity !== 1 123 ? color(contentColor) 124 .alpha(titleTextOpacity) 125 .toString() 126 : contentColor 127 }> 128 {children} 129 </ThemeText> 130 ) : ( 131 children 132 )} 133 </View> 134 {actions.map((action, index) => ( 135 <IconButton 136 key={index} 137 name={action.icon} 138 sizeType="normal" 139 color={contentColor} 140 style={[globalStyle.notShrink, styles.rightButton]} 141 onPress={action.onPress} 142 /> 143 ))} 144 {actionComponent ?? null} 145 {menu?.length ? ( 146 <IconButton 147 name="ellipsis-vertical" 148 sizeType="normal" 149 onLayout={evt => { 150 setMenuIconLayout(evt.nativeEvent.layout); 151 }} 152 color={contentColor} 153 style={[globalStyle.notShrink, styles.rightButton]} 154 onPress={() => { 155 setShowMenu(true); 156 }} 157 /> 158 ) : null} 159 </View> 160 <Portal> 161 {showMenu ? ( 162 <TouchableWithoutFeedback 163 onPress={() => { 164 setShowMenu(false); 165 }}> 166 <View style={styles.blocker} /> 167 </TouchableWithoutFeedback> 168 ) : null} 169 <> 170 <Animated.View 171 pointerEvents={showMenu ? 'auto' : 'none'} 172 style={[ 173 { 174 borderBottomColor: colors.background, 175 left: 176 (menuIconLayout?.x ?? 0) + 177 (menuIconLayout?.width ?? 0) / 2 - 178 rpx(10), 179 top: 180 (menuIconLayout?.y ?? 0) + 181 (menuIconLayout?.height ?? 0) + 182 (menuWithStatusBar 183 ? OriginalStatusBar.currentHeight ?? 0 184 : 0), 185 }, 186 transformStyle, 187 styles.bubbleCorner, 188 ]} 189 /> 190 <Animated.View 191 pointerEvents={showMenu ? 'auto' : 'none'} 192 style={[ 193 { 194 backgroundColor: colors.background, 195 right: rpx(24), 196 top: 197 (menuIconLayout?.y ?? 0) + 198 (menuIconLayout?.height ?? 0) + 199 rpx(20) + 200 (menuWithStatusBar 201 ? OriginalStatusBar.currentHeight ?? 0 202 : 0), 203 shadowColor: colors.shadow, 204 }, 205 transformStyle, 206 styles.menu, 207 ]}> 208 {menu.map(it => 209 it.show !== false ? ( 210 <ListItem 211 key={it.title} 212 withHorizontalPadding 213 heightType="small" 214 onPress={() => { 215 it.onPress?.(); 216 setShowMenu(false); 217 }}> 218 <ListItem.ListItemIcon icon={it.icon} /> 219 <ListItem.Content title={it.title} /> 220 </ListItem> 221 ) : null, 222 )} 223 </Animated.View> 224 </> 225 </Portal> 226 </> 227 ); 228} 229 230const styles = StyleSheet.create({ 231 container: { 232 width: '100%', 233 zIndex: 10000, 234 height: rpx(88), 235 flexDirection: 'row', 236 alignItems: 'center', 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