import { Link } from '@react-navigation/native' import * as React from 'react' import { Animated, Dimensions, EmitterSubscription, Keyboard, LayoutChangeEvent, Platform, StyleSheet, TouchableWithoutFeedback, View } from 'react-native' import { EdgeInsets, withSafeAreaInsets, WithSafeAreaInsetsProps } from 'react-native-safe-area-context' import { getDefaultTabItem, getTabConfig, getTabItemConfig, getTabVisible, isUrl } from '../utils/index' import TabBarItem, { TabBarOptions, TabOptions } from './TabBarItem' import { getInitSafeAreaInsets } from './tabBarUtils' interface TabBarProps extends TabBarOptions { state: Record navigation: any descriptors: Record tabOptions: TabOptions } interface TabBarState { visible: Animated.Value isKeyboardShown: boolean tabVisible: boolean layout: { height: number width: number } insets: EdgeInsets } interface TabBarStyle { color?: string selectedColor?: string backgroundColor?: string borderStyle?: string } const DEFAULT_TABBAR_HEIGHT = 55 const COMPACT_TABBAR_HEIGHT = 32 // const DEFAULT_MAX_TAB_ITEM_WIDTH = 125; const useNativeDriver = Platform.OS !== 'web' const styles = StyleSheet.create({ tabBar: { left: 0, right: 0, bottom: 0, borderTopWidth: StyleSheet.hairlineWidth, elevation: 8 }, tab: { flex: 1, alignItems: 'center' }, tabPortrait: { justifyContent: 'flex-end', flexDirection: 'column' }, tabLandscape: { justifyContent: 'center', flexDirection: 'row' } }) export class TabBar extends React.PureComponent { handleKeyboardShowEvent: EmitterSubscription handleKeyboardHideEvent: EmitterSubscription constructor (props: TabBarProps & WithSafeAreaInsetsProps) { super(props) const { height = 0, width = 0 } = Dimensions.get('window') const { insets, safeAreaInsets, tabOptions = {} } = this.props const { tabBarVisible = true } = tabOptions const tabVisible = tabBarVisible === false ? false : getTabVisible() this.state = { visible: new Animated.Value(tabVisible ? 1 : 0), tabVisible: tabVisible, isKeyboardShown: false, layout: { width, height }, // todo: remove safeAreaInsets insets: insets || safeAreaInsets || getInitSafeAreaInsets() } } componentDidMount () { const { keyboardHidesTabBar = false } = this.props if (keyboardHidesTabBar) { if (Platform.OS === 'ios') { this.handleKeyboardShowEvent = Keyboard.addListener('keyboardWillShow', () => this.handleKeyboardShow()) this.handleKeyboardHideEvent = Keyboard.addListener('keyboardWillHide', () => this.handleKeyboardHide()) } else { this.handleKeyboardShowEvent = Keyboard.addListener('keyboardDidShow', () => this.handleKeyboardShow()) this.handleKeyboardHideEvent = Keyboard.addListener('keyboardDidHide', () => this.handleKeyboardHide()) } } } componentWillUnmount () { const { keyboardHidesTabBar = false } = this.props if (keyboardHidesTabBar) { this.handleKeyboardShowEvent.remove() this.handleKeyboardHideEvent.remove() } } UNSAFE_componentWillReceiveProps (nextProps): void { const curVisible = getTabVisible() const { tabVisible, insets } = this.state if (curVisible !== tabVisible) { this.setState({ tabVisible: curVisible }) this.setTabBarHidden(!curVisible) } if (nextProps.insets && insets !== nextProps.insets) { this.setState({ insets: nextProps.insets }) } } handleKeyboardShow () { this.setState({ isKeyboardShown: true, tabVisible: false }) this.setTabBarHidden(true) } handleKeyboardHide () { this.setState({ isKeyboardShown: false, tabVisible: getTabVisible() }) this.setTabBarHidden(false) } setTabBarHidden (isHidden: boolean) { if (!getTabConfig('needAnimate')) return const { visible } = this.state if (isHidden) { this.setState({ tabVisible: false }) Animated.timing(visible, { toValue: 0, duration: 200, useNativeDriver }).start() } else { Animated.timing(visible, { toValue: 1, duration: 250, useNativeDriver }).start(({ finished }) => { if (finished) { this.setState({ tabVisible: true }) } }) } } isLandscape (): boolean { const { height = 0, width = 0 } = Dimensions.get('window') return width > height } getDefaultTabBarHeight (): number { if (Platform.OS === 'ios' && !Platform.isPad && this.isLandscape()) { return COMPACT_TABBAR_HEIGHT } return DEFAULT_TABBAR_HEIGHT } // 只有最简单的格式 buildLink (name: string, params: Record): string { const keys = Object.keys(params).sort() let str = '' keys.forEach((v) => { str += (v + '=' + encodeURIComponent(params[v]) + '&') }) str = str.slice(0, str.length - 1) return str ? `${name}?${str}` : `${name}` } handleLayout (e: LayoutChangeEvent): void { const { layout } = this.state const { height, width } = e.nativeEvent.layout if (layout.height !== height && layout.width !== width) { this.setState({ layout: { width, height } }) } } getTabBarStyle (): Record { const { activeTintColor, inactiveTintColor, style = {} } = this.props const tabStyle = getTabConfig('tabStyle') as TabBarStyle const { color = '', selectedColor = '', backgroundColor = '', borderStyle = '' } = tabStyle const defaultBackground: any = style?.backgroundColor || 'rgb(255, 255, 255)' const defalutBorderTopColor: any = style?.borderTopColor || 'rgb(216, 216, 216)' return { backgroundColor: backgroundColor || defaultBackground, borderTopColor: borderStyle || defalutBorderTopColor, color: color || inactiveTintColor, selectedColor: selectedColor || activeTintColor } } getTabIconSource (index: number, focused: boolean) { const item: any = getDefaultTabItem(index) const iconPath = getTabItemConfig(index, 'iconPath') ?? item?.iconPath const selectedIconPath = getTabItemConfig(index, 'selectedIconPath') ?? item?.selectedIconPath const path = focused ? selectedIconPath : iconPath return isUrl(path) ? { uri: path } : { uri: path } } renderContent () { const { state, descriptors, navigation } = this.props const horizontal = true const tabSelfStyle = this.getTabBarStyle() return this.handleLayout}> {state.routes.map((route, index) => { const focused = index === state.index const { options } = descriptors[route.key] const tabLabel = getTabItemConfig(index, 'tabBarLabel') const label = tabLabel || (options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined ? options.title : route.name) const accessibilityLabel = options.tabBarAccessibilityLabel !== undefined ? options.tabBarAccessibilityLabel : typeof label === 'string' ? `${label}, tab, ${index + 1} of ${state.routes.length}` : undefined const onPress = () => { const event = navigation.emit({ type: 'tabPress', target: route.key }) if (!focused && !event.defaultPrevented) { navigation.navigate(route.name) } } const onLongPress = () => { navigation.emit({ type: 'tabLongPress', target: route.key }) } const badge = getTabItemConfig(index, 'tabBarBadge') const showRedDot = getTabItemConfig(index, 'showRedDot') || false const labelColor = focused ? tabSelfStyle.selectedColor : tabSelfStyle.color const source = this.getTabIconSource(index, focused) if (Platform.OS === 'web') { const linkTo = this.buildLink(route.name, route.params) return ( { if ( !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && (e.button == null || e.button === 0) ) { e.preventDefault() onPress() } }} > ) } else { return ( ) } })} } render () { const { insets, visible, layout, tabVisible, isKeyboardShown } = this.state const paddingBottom = Math.max( insets?.bottom - Platform.select({ ios: 4, default: 0 }), 5) const { style } = this.props const tabBarStyle = this.getTabBarStyle() const needAnimate = getTabConfig('needAnimate') const showTabBar = tabVisible !== false && !isKeyboardShown if (!needAnimate) { return (!showTabBar ? null : ( {this.renderContent()} ) ) } else { return ( {this.renderContent()} ) } } } export default withSafeAreaInsets(TabBar)