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