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