import { BottomTabBarProps } from "@react-navigation/bottom-tabs"; import { BlurView } from "expo-blur"; import * as Haptics from "expo-haptics"; import React, { useEffect, useRef } from "react"; import { LayoutChangeEvent, Platform, StyleSheet, Text, TouchableOpacity, View } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { IconSymbol } from "@/components/ui/icon-symbol"; import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; // 图标名称映射 const TAB_ICONS: Record = { index: "list", live: "play-circle", upcoming: "calendar", finished: "checkmark-circle", favorite: "star", }; // 将十六进制颜色转换为带透明度的 rgba function hexToRgba(hex: string, alpha: number): string { // 移除 # 符号 const cleanHex = hex.replace("#", ""); // 处理 3 位和 6 位十六进制颜色 const r = cleanHex.length === 3 ? parseInt(cleanHex[0] + cleanHex[0], 16) : parseInt(cleanHex.substring(0, 2), 16); const g = cleanHex.length === 3 ? parseInt(cleanHex[1] + cleanHex[1], 16) : parseInt(cleanHex.substring(2, 4), 16); const b = cleanHex.length === 3 ? parseInt(cleanHex[2] + cleanHex[2], 16) : parseInt(cleanHex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } interface TabItemProps { route: BottomTabBarProps["state"]["routes"][0]; isFocused: boolean; onPress: () => void; label: string; activeColor: string; inactiveColor: string; } interface TabItemPropsWithLayout extends TabItemProps { onLayout: (event: LayoutChangeEvent) => void; } const TabItem = ({ route, isFocused, onPress, label, activeColor, inactiveColor, onLayout, }: TabItemPropsWithLayout) => { const iconName = TAB_ICONS[route.name] || "list"; return ( {label} ); }; export default function CustomTabBar({ state, descriptors, navigation, }: BottomTabBarProps) { const { theme } = useTheme(); const insets = useSafeAreaInsets(); const isDark = theme === "dark"; // 图标和文字颜色:light 和 dark 模式都使用主题色 const activeColor = Colors.light.tint; const inactiveColor = Colors[theme].tabIconDefault; // 胶囊背景颜色:light 模式用浅白色,dark 模式用浅黑色 const indicatorBackgroundColor = isDark ? "rgba(40, 40, 40, 0.6)" // 浅黑色 : "rgba(240, 240, 240, 0.8)"; // 浅白色 // 计算底部安全区域 const bottomInset = Platform.OS === "ios" ? insets.bottom : 20; // 胶囊背景位置动画 const indicatorPosition = useSharedValue(0); const tabLayouts = useRef<{ [key: number]: { x: number; width: number } }>({}); const containerWidth = useRef(0); // 胶囊背景宽度(动态,等于每个 tab 的宽度减去左右 margin) const indicatorWidth = useSharedValue(0); // 胶囊背景的 margin const INDICATOR_MARGIN_HORIZONTAL = 4; // 左右 margin const INDICATOR_MARGIN_VERTICAL = 4; // 上下 margin(设为 0 让胶囊背景更高) // 当选中 tab 改变时,更新胶囊背景位置 useEffect(() => { const currentIndex = state.index; const tabLayout = tabLayouts.current[currentIndex]; if (tabLayout && containerWidth.current > 0) { // 计算带 margin 的位置和宽度 const targetX = tabLayout.x + INDICATOR_MARGIN_HORIZONTAL; const targetWidth = tabLayout.width - INDICATOR_MARGIN_HORIZONTAL * 2; // 使用 withTiming 实现平滑的线性移动动画 indicatorPosition.value = withTiming(targetX, { duration: 200, }); indicatorWidth.value = withTiming(targetWidth, { duration: 200, }); } }, [state.index]); // 处理容器布局 const handleContainerLayout = (event: LayoutChangeEvent) => { containerWidth.current = event.nativeEvent.layout.width; }; // 处理每个 tab 的布局事件 const handleTabLayout = (index: number) => (event: LayoutChangeEvent) => { const { x, width } = event.nativeEvent.layout; tabLayouts.current[index] = { x, width }; // 如果是当前选中的 tab,立即更新位置 if (index === state.index && containerWidth.current > 0) { const INDICATOR_MARGIN_HORIZONTAL = 4; indicatorPosition.value = x + INDICATOR_MARGIN_HORIZONTAL; indicatorWidth.value = width - INDICATOR_MARGIN_HORIZONTAL * 2; } }; // 胶囊背景动画样式 const indicatorStyle = useAnimatedStyle(() => { return { transform: [{ translateX: indicatorPosition.value }], width: indicatorWidth.value, }; }); return ( {/* 胶囊背景指示器 */} {state.routes.map((route, index) => { const { options } = descriptors[route.key]; const isFocused = state.index === index; const onPress = () => { // 触发震动反馈 (iOS Impact Light 非常细腻) if (Platform.OS === "ios") { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } const event = navigation.emit({ type: "tabPress", target: route.key, canPreventDefault: true, }); if (!isFocused && !event.defaultPrevented) { navigation.navigate(route.name); } }; // 获取标签文本,优先使用 tabBarLabel,然后是 title let label: string; if (options.tabBarLabel !== undefined) { label = typeof options.tabBarLabel === "string" ? options.tabBarLabel : route.name; } else if (options.title !== undefined) { label = options.title; } else { label = route.name; } return ( ); })} ); } const styles = StyleSheet.create({ safeArea: { position: "absolute", left: 12, right: 12, }, tabBarContainer: { flexDirection: "row", height: 60, // 从 56 增加到 58 borderRadius: 30, // 相应调整圆角 overflow: "hidden", borderWidth: 0.5, }, tabItem: { flex: 1, alignItems: "center", justifyContent: "center", }, iconContainer: { width: 48, height: 28, alignItems: "center", justifyContent: "center", marginBottom: 2, }, indicator: { position: "absolute", height: 52, // 58 (tabBarContainer 高度) - 8 (上下 margin 4px * 2) borderRadius: 26, // 与 tabBarContainer 的 borderRadius 一致 top: 4, // 上 margin }, tabLabel: { fontSize: 10, fontWeight: "600", }, });