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 setShowMenu(false); 216 // async 217 setTimeout(() => { 218 it.onPress?.(); 219 }, 20); 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 alignItems: 'center', 240 paddingHorizontal: rpx(24), 241 }, 242 content: { 243 flexDirection: 'row', 244 flexBasis: 0, 245 alignItems: 'center', 246 paddingHorizontal: rpx(24), 247 }, 248 rightButton: { 249 marginLeft: rpx(28), 250 }, 251 blocker: { 252 position: 'absolute', 253 bottom: 0, 254 left: 0, 255 width: '100%', 256 height: '100%', 257 zIndex: 10010, 258 }, 259 bubbleCorner: { 260 position: 'absolute', 261 borderColor: 'transparent', 262 borderWidth: rpx(10), 263 zIndex: 10012, 264 transformOrigin: 'right top', 265 opacity: 0, 266 }, 267 menu: { 268 width: rpx(340), 269 maxHeight: rpx(600), 270 borderRadius: rpx(8), 271 zIndex: 10011, 272 position: 'absolute', 273 opacity: 0, 274 shadowOffset: { 275 width: 0, 276 height: 2, 277 }, 278 shadowOpacity: 0.23, 279 shadowRadius: 2.62, 280 elevation: 4, 281 }, 282}); 283