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} 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 backgroundOpacity = 1, 60 titleTextOpacity = 1, 61 withStatusBar, 62 color: _color, 63 actions = [], 64 menu = [], 65 menuWithStatusBar = true, 66 containerStyle, 67 contentStyle, 68 children, 69 } = props; 70 71 const colors = useColors(); 72 const navigation = useNavigation(); 73 74 const bgColor = color(colors.appBar ?? colors.primary) 75 .alpha(backgroundOpacity) 76 .toString(); 77 const contentColor = _color ?? colors.headerText; 78 79 const [showMenu, setShowMenu] = useState(false); 80 const menuIconLayoutRef = useRef<LayoutRectangle>(); 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 => ( 135 <IconButton 136 name={action.icon} 137 sizeType="normal" 138 color={contentColor} 139 style={[globalStyle.notShrink, styles.rightButton]} 140 onPress={action.onPress} 141 /> 142 ))} 143 {menu?.length ? ( 144 <IconButton 145 name="dots-vertical" 146 sizeType="normal" 147 onLayout={e => { 148 menuIconLayoutRef.current = e.nativeEvent.layout; 149 }} 150 color={contentColor} 151 style={[globalStyle.notShrink, styles.rightButton]} 152 onPress={() => { 153 setShowMenu(true); 154 }} 155 /> 156 ) : null} 157 </View> 158 <Portal> 159 {showMenu ? ( 160 <TouchableWithoutFeedback 161 onPress={() => { 162 setShowMenu(false); 163 }}> 164 <View style={styles.blocker} /> 165 </TouchableWithoutFeedback> 166 ) : null} 167 <> 168 <Animated.View 169 pointerEvents={showMenu ? 'auto' : 'none'} 170 style={[ 171 { 172 borderBottomColor: colors.backdrop, 173 left: 174 (menuIconLayoutRef.current?.x ?? 0) + 175 (menuIconLayoutRef.current?.width ?? 0) / 176 2 - 177 rpx(10), 178 top: 179 (menuIconLayoutRef.current?.y ?? 0) + 180 (menuIconLayoutRef.current?.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 (menuIconLayoutRef.current?.y ?? 0) + 197 (menuIconLayoutRef.current?.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 withHorizonalPadding 211 heightType="small" 212 onPress={() => { 213 it.onPress?.(); 214 setShowMenu(false); 215 }}> 216 <ListItem.ListItemIcon icon={it.icon} /> 217 <ListItem.Content title={it.title} /> 218 </ListItem> 219 ) : null, 220 )} 221 </Animated.View> 222 </> 223 </Portal> 224 </> 225 ); 226} 227 228const styles = StyleSheet.create({ 229 container: { 230 width: '100%', 231 zIndex: 10000, 232 height: rpx(88), 233 flexDirection: 'row', 234 paddingHorizontal: rpx(24), 235 }, 236 content: { 237 flexDirection: 'row', 238 flexBasis: 0, 239 alignItems: 'center', 240 paddingHorizontal: rpx(24), 241 }, 242 rightButton: { 243 marginLeft: rpx(28), 244 }, 245 blocker: { 246 position: 'absolute', 247 bottom: 0, 248 left: 0, 249 width: '100%', 250 height: '100%', 251 zIndex: 10010, 252 }, 253 bubbleCorner: { 254 position: 'absolute', 255 borderColor: 'transparent', 256 borderWidth: rpx(10), 257 zIndex: 10012, 258 transformOrigin: 'right top', 259 opacity: 0, 260 }, 261 menu: { 262 width: rpx(340), 263 maxHeight: rpx(600), 264 borderRadius: rpx(8), 265 zIndex: 10011, 266 position: 'absolute', 267 opacity: 0, 268 shadowOffset: { 269 width: 0, 270 height: 2, 271 }, 272 shadowOpacity: 0.23, 273 shadowRadius: 2.62, 274 elevation: 4, 275 }, 276}); 277