import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { NavigationContainer, NavigationIndependentTree, useFocusEffect, } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Animated, Platform, StatusBar, StyleSheet, useWindowDimensions, View, } from 'react-native'; import { Colors } from '../../Consts'; import { Icon } from '../../Icon'; import { exportFontFamily, Text } from '../../Text'; import StackScreen from '../StackScreen'; import { BottomTabProps } from '../types'; import { getOptions, getStackOptions, useAppState } from '../utils'; import BottomTabBar from './BottomTabBar'; import { ApplicationContext, MiniAppContext } from '../../Context'; const Tab = createBottomTabNavigator(); const Stack = createStackNavigator(); const TabScreen: React.FC = ({ route, navigation }) => { const { navigator } = useContext(ApplicationContext); const context = useContext(MiniAppContext); const { nested, options, screen, initialParams } = route?.params; const insets = useSafeAreaInsets(); const opacityValue = 0.3; const scaleValue = 0.97; const opacity = useRef(new Animated.Value(opacityValue)).current; const scale = useRef(new Animated.Value(scaleValue)).current; const screenName = screen?.name || screen?.type?.name || 'Invalid'; const { isBackgroundToForeground } = useAppState(); const onScreenNavigated = (pre: string, current: string) => { if (!isBackgroundToForeground) { const data: any = { preScreenName: pre, screenName: current, componentName: 'Screen', state: 'navigated', action: 'push', }; context?.autoTracking?.({ ...context, ...data, }); /** * debug toast */ navigator?.maxApi?.showToastDebug?.({ appId: context.appId, message: `${screenName} screen_navigated`, type: 'ERROR', }); } }; useFocusEffect( React.useCallback( () => { Animated.parallel([ Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true, }), Animated.timing(scale, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); setTimeout(() => { navigator?.maxApi?.getDataObserver?.( 'current_screen', (data: any) => { onScreenNavigated(data?.screenName, screenName); navigator?.maxApi?.setObserver?.('current_screen', { screenName, }); }, ); }, 100); return () => { if (navigation.getState().index !== route?.params.index) { Animated.parallel([ Animated.timing(opacity, { toValue: opacityValue, duration: 200, useNativeDriver: true, }), Animated.timing(scale, { toValue: scaleValue, duration: 200, useNativeDriver: true, }), ]).start(); } }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ), ); useEffect( () => { const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => { if (navigation.isFocused()) { navigator?.maxApi?.getDataObserver?.( 'current_screen', (data: any) => { onScreenNavigated(data?.screenName, screenName); navigator?.maxApi?.setObserver?.('current_screen', { screenName, }); }, ); } }); return () => { onFocusApp?.remove?.(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); let stackOptions = {}; if (options) { stackOptions = getOptions(options); } if (nested) { return ( ); } return ( ); }; const BottomTab: React.FC = ({ nested, tabs, listeners, navigation, initialRouteName, floatingButton, }) => { const { theme, navigator } = useContext(ApplicationContext); const dimensions = useWindowDimensions(); const insets = useSafeAreaInsets(); const initialIndex = tabs.findIndex(i => i.name === initialRouteName); const indicatorAnimated = useRef(new Animated.Value(0)); const activeIndex = useRef(initialIndex > 0 ? initialIndex : 0); const itemWidth = dimensions.width / tabs.length; useLayoutEffect(() => { navigation?.setOptions({ headerShown: false, headerTransparent: true, headerBackground: undefined, }); }, [navigation]); useEffect(() => { Animated.timing(indicatorAnimated.current, { toValue: itemWidth * activeIndex.current, useNativeDriver: true, duration: 200, }).start(); }, [itemWidth]); const onFocus = (e: any) => { activeIndex.current = tabs.findIndex(i => e.target.includes(i.name)); Animated.timing(indicatorAnimated.current, { toValue: itemWidth * activeIndex.current, useNativeDriver: true, duration: 200, }).start(); }; const handler: { tabPress?: (e: any) => void; focus?: (e: any) => void; blur?: (e: any) => void; state?: (e: any) => void; } = { tabPress: e => { navigator?.maxApi?.triggerEventVibration?.('light'); listeners?.tabPress?.(e); }, focus: e => { onFocus(e); StatusBar.setBarStyle('dark-content', true); listeners?.focus?.(e); }, blur: e => { listeners?.blur?.(e); }, }; return ( ( ), }} tabBar={props => ( )} initialRouteName={initialRouteName} > {tabs.map((item, index) => { const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10)); const isDot = typeof item?.badgeLabel === 'string' && !item?.badgeLabel; const isNumZero = isNum && Number(item.badgeLabel) === 0; let badgeLabel = item?.badgeLabel; if (isNumZero) { badgeLabel = undefined; } let badgeStyle: { marginRight: number; top: number; backgroundColor: string; width?: number; } = { marginRight: -8, top: -4, backgroundColor: Colors.orange_03, }; if (isNum) { badgeStyle = { marginRight: -4, top: -4, backgroundColor: Colors.red_03, }; } if (isDot) { badgeStyle = { marginRight: -2, top: -2, backgroundColor: Colors.orange_03, width: 12, }; } return ( { return ( {item.label} ); }, title: item.label, tabBarStyle: [ { height: 64, }, styles.tabStyle, ], tabBarBadge: badgeLabel, tabBarBadgeStyle: [ { borderColor: 'white', borderWidth: 2, fontSize: 10, lineHeight: 14, fontWeight: 'bold', alignSelf: 'center', fontFamily: exportFontFamily('bold'), }, badgeStyle, ], tabBarIcon: ({ color }) => ( ), ...item, }} /> ); })} ); }; const styles = StyleSheet.create({ container: { flex: 1 }, indicatorContainer: { position: 'absolute', top: 0 }, indicator: { height: 2, width: 48, alignSelf: 'center', borderBottomLeftRadius: 2, borderBottomRightRadius: 2, }, tabBarStyle: { ...Platform.select({ ios: { shadowColor: Colors.black_10, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 4, }, android: { elevation: 4, }, }), }, tabStyle: { paddingVertical: 4, }, }); export default BottomTab;