You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.5 KiB
178 lines
5.5 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { BlurView } from 'expo-blur';
|
|
import { Tabs } from 'expo-router/js-tabs';
|
|
import type { BottomTabBarProps } from 'expo-router/js-tabs';
|
|
import { useEffect, useRef } from 'react';
|
|
import type { ComponentProps, ReactNode } from 'react';
|
|
import { Animated, Pressable, StyleSheet, Text, View, type ColorValue } from 'react-native';
|
|
import { colors } from '../../src/theme';
|
|
|
|
type IconName = ComponentProps<typeof Ionicons>['name'];
|
|
type TabIconProps = { focused: boolean; color: ColorValue; size: number };
|
|
|
|
/** 内容底部预留高度:悬浮 Tab 栏高度 + 悬浮间距 + 余量 */
|
|
const TAB_BAR_FLOAT_SPACE = 84;
|
|
|
|
/**
|
|
* 选中/未选中切换实心与描边图标
|
|
*/
|
|
function tabIcon(filled: IconName, outline: IconName) {
|
|
return ({ color, size, focused }: TabIconProps) => (
|
|
<Ionicons name={focused ? filled : outline} size={size} color={color} />
|
|
);
|
|
}
|
|
|
|
/** 单个 Tab 项:选中时图标轻弹 + 浅蓝胶囊高亮 */
|
|
function TabItem({
|
|
focused,
|
|
label,
|
|
icon,
|
|
onPress,
|
|
onLongPress,
|
|
}: {
|
|
focused: boolean;
|
|
label: string;
|
|
icon?: (props: TabIconProps) => ReactNode;
|
|
onPress: () => void;
|
|
onLongPress: () => void;
|
|
}) {
|
|
// 图标弹跳动画:选中 1.15,未选中回到 1
|
|
const scale = useRef(new Animated.Value(1)).current;
|
|
useEffect(() => {
|
|
Animated.spring(scale, {
|
|
toValue: focused ? 1.15 : 1,
|
|
useNativeDriver: true,
|
|
friction: 6,
|
|
tension: 200,
|
|
}).start();
|
|
}, [focused, scale]);
|
|
|
|
const color = focused ? colors.primary : colors.textSecondary;
|
|
return (
|
|
<Pressable
|
|
style={styles.tabItem}
|
|
onPress={onPress}
|
|
onLongPress={onLongPress}
|
|
accessibilityRole="button"
|
|
accessibilityState={{ selected: focused }}
|
|
>
|
|
<View style={[styles.tabItemContent, focused && styles.tabItemContentActive]}>
|
|
<Animated.View style={{ transform: [{ scale }] }}>
|
|
{icon?.({ focused, color, size: 22 })}
|
|
</Animated.View>
|
|
<Text style={[styles.tabLabel, { color }]}>{label}</Text>
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/** 悬浮毛玻璃 Tab 栏 */
|
|
function FloatingTabBar({ state, descriptors, navigation, insets }: BottomTabBarProps) {
|
|
return (
|
|
<View style={styles.tabBarWrap} pointerEvents="box-none">
|
|
<View style={[styles.tabBarShadow, { marginBottom: insets.bottom + 12 }]}>
|
|
<BlurView intensity={80} tint="light" blurMethod="dimezisBlurViewSdk31Plus" style={styles.tabBar}>
|
|
{state.routes.map((route, index) => {
|
|
const { options } = descriptors[route.key];
|
|
const focused = state.index === index;
|
|
const label =
|
|
typeof options.tabBarLabel === 'string' ? options.tabBarLabel : options.title ?? '';
|
|
|
|
const onPress = () => {
|
|
const event = navigation.emit({
|
|
type: 'tabPress',
|
|
target: route.key,
|
|
canPreventDefault: true,
|
|
});
|
|
if (!focused && !event.defaultPrevented) {
|
|
navigation.navigate(route.name);
|
|
}
|
|
};
|
|
const onLongPress = () =>
|
|
navigation.emit({ type: 'tabLongPress', target: route.key });
|
|
|
|
return (
|
|
<TabItem
|
|
key={route.key}
|
|
focused={focused}
|
|
label={label}
|
|
icon={options.tabBarIcon}
|
|
onPress={onPress}
|
|
onLongPress={onLongPress}
|
|
/>
|
|
);
|
|
})}
|
|
</BlurView>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 底部 Tab 导航:悬浮毛玻璃胶囊 + 选中高亮 + 图标弹跳
|
|
*/
|
|
export default function TabLayout() {
|
|
return (
|
|
<Tabs
|
|
screenOptions={{
|
|
headerShown: false,
|
|
// 悬浮 Tab 栏不占布局空间,给每个页面内容底部预留空间,避免被遮挡
|
|
sceneStyle: { paddingBottom: TAB_BAR_FLOAT_SPACE },
|
|
}}
|
|
tabBar={(props) => <FloatingTabBar {...props} />}
|
|
>
|
|
<Tabs.Screen name="index" options={{ title: '首页', tabBarIcon: tabIcon('home', 'home-outline') }} />
|
|
<Tabs.Screen name="jobs" options={{ title: '岗位', tabBarIcon: tabIcon('briefcase', 'briefcase-outline') }} />
|
|
<Tabs.Screen name="chat" options={{ title: '沟通', tabBarIcon: tabIcon('chatbubbles', 'chatbubbles-outline') }} />
|
|
<Tabs.Screen name="growth" options={{ title: '成长', tabBarIcon: tabIcon('trending-up', 'trending-up-outline') }} />
|
|
<Tabs.Screen name="profile" options={{ title: '我的', tabBarIcon: tabIcon('person', 'person-outline') }} />
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
tabBarWrap: {
|
|
position: 'absolute',
|
|
left: 16,
|
|
right: 16,
|
|
bottom: 0,
|
|
},
|
|
// 外层负责 iOS 阴影(不裁剪,阴影在圆角外正常显示)
|
|
tabBarShadow: {
|
|
borderRadius: 24,
|
|
shadowColor: colors.textPrimary,
|
|
shadowOffset: { width: 0, height: 6 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 16,
|
|
},
|
|
// 内层 BlurView:真毛玻璃(iOS)/ 半透明(Android 31 以下降级)+ Android 阴影
|
|
tabBar: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
borderRadius: 24,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 8,
|
|
overflow: 'hidden',
|
|
elevation: 10,
|
|
},
|
|
tabItem: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
tabItemContent: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 18,
|
|
},
|
|
tabItemContentActive: {
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
tabLabel: {
|
|
marginTop: 4,
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
},
|
|
});
|
|
|