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 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 menuIconLayoutRef = useRef<LayoutRectangle>(); 79 const scaleRate = useSharedValue(0); 80 81 useEffect(() => { 82 if (showMenu) { 83 scaleRate.value = withTiming(1, timingConfig); 84 } else { 85 scaleRate.value = withTiming(0, timingConfig); 86 } 87 }, [showMenu]); 88 89 const transformStyle = useAnimatedStyle(() => { 90 return { 91 opacity: scaleRate.value, 92 }; 93 }); 94 95 return ( 96 <> 97 {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null} 98 <View 99 style={[ 100 styles.container, 101 containerStyle, 102 {backgroundColor: bgColor}, 103 ]}> 104 <IconButton 105 name="arrow-left" 106 sizeType="normal" 107 color={contentColor} 108 style={globalStyle.notShrink} 109 onPress={() => { 110 navigation.goBack(); 111 }} 112 /> 113 <View style={[globalStyle.grow, styles.content, contentStyle]}> 114 {typeof children === 'string' ? ( 115 <ThemeText 116 fontSize="title" 117 fontWeight="bold" 118 numberOfLines={1} 119 color={ 120 titleTextOpacity !== 1 121 ? color(contentColor) 122 .alpha(titleTextOpacity) 123 .toString() 124 : contentColor 125 }> 126 {children} 127 </ThemeText> 128 ) : ( 129 children 130 )} 131 </View> 132 {actions.map((action, index) => ( 133 <IconButton 134 key={index} 135 name={action.icon} 136 sizeType="normal" 137 color={contentColor} 138 style={[globalStyle.notShrink, styles.rightButton]} 139 onPress={action.onPress} 140 /> 141 ))} 142 {actionComponent ?? null} 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.background, 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 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