feat: 新增动画资源与庆祝效果,优化布局与标签页配置
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Tabs, usePathname } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
|
||||
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
@@ -9,270 +10,138 @@ import { ROUTES } from '@/constants/Routes';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
// Tab configuration
|
||||
type TabConfig = {
|
||||
icon: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
||||
explore: { icon: 'magnifyingglass.circle.fill', title: '发现' },
|
||||
goals: { icon: 'flag.fill', title: '习惯' },
|
||||
personal: { icon: 'person.fill', title: '个人' },
|
||||
};
|
||||
|
||||
export default function TabLayout() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const pathname = usePathname();
|
||||
|
||||
// Helper function to determine if a tab is selected
|
||||
const isTabSelected = (routeName: string): boolean => {
|
||||
const routeMap: Record<string, string> = {
|
||||
explore: ROUTES.TAB_EXPLORE,
|
||||
goals: ROUTES.TAB_GOALS,
|
||||
statistics: ROUTES.TAB_STATISTICS,
|
||||
};
|
||||
|
||||
return routeMap[routeName] === pathname || pathname.includes(routeName);
|
||||
};
|
||||
|
||||
// Custom tab button component
|
||||
const createTabButton = (routeName: string) => (props: any) => {
|
||||
const { onPress } = props;
|
||||
const tabConfig = TAB_CONFIGS[routeName];
|
||||
|
||||
if (!tabConfig) return null;
|
||||
|
||||
const isSelected = isTabSelected(routeName);
|
||||
|
||||
const handlePress = (event: any) => {
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
onPress?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 6,
|
||||
marginVertical: 10,
|
||||
borderRadius: 25,
|
||||
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
|
||||
paddingHorizontal: isSelected ? 16 : 10,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol
|
||||
size={22}
|
||||
name={tabConfig.icon as any}
|
||||
color={isSelected ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
|
||||
/>
|
||||
{isSelected && (
|
||||
<Text
|
||||
style={{
|
||||
color: colorTokens.tabIconSelected,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
}}
|
||||
numberOfLines={0 as any}
|
||||
>
|
||||
{tabConfig.title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// Common screen options
|
||||
const getScreenOptions = (routeName: string): BottomTabNavigationOptions => ({
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
||||
tabBarButton: createTabButton(routeName),
|
||||
tabBarStyle: {
|
||||
position: 'absolute',
|
||||
bottom: TAB_BAR_BOTTOM_OFFSET,
|
||||
height: TAB_BAR_HEIGHT,
|
||||
borderRadius: 34,
|
||||
backgroundColor: colorTokens.tabBarBackground,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 10,
|
||||
elevation: 5,
|
||||
paddingHorizontal: 10,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginHorizontal: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
alignSelf: 'center',
|
||||
} as ViewStyle,
|
||||
tabBarItemStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
height: TAB_BAR_HEIGHT,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
tabBarShowLabel: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
initialRouteName="statistics"
|
||||
screenOptions={({ route }) => {
|
||||
const routeName = route.name;
|
||||
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
||||
(routeName === 'coach' && pathname === ROUTES.TAB_COACH) ||
|
||||
(routeName === 'goals' && pathname === ROUTES.TAB_GOALS) ||
|
||||
(routeName === 'statistics' && pathname === ROUTES.TAB_STATISTICS) ||
|
||||
pathname.includes(routeName);
|
||||
screenOptions={({ route }) => getScreenOptions(route.name)}
|
||||
>
|
||||
|
||||
return {
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
||||
tabBarButton: (props) => {
|
||||
const { onPress } = props;
|
||||
|
||||
const handlePress = (event: any) => {
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
onPress && onPress(event);
|
||||
};
|
||||
|
||||
// 基于 routeName 设置图标与标题,避免 tabBarIcon 的包装导致文字裁剪
|
||||
const getIconAndTitle = () => {
|
||||
switch (routeName) {
|
||||
case 'explore':
|
||||
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
|
||||
case 'coach':
|
||||
return { icon: 'message.fill', title: 'AI' } as const;
|
||||
case 'goals':
|
||||
return { icon: 'flag.fill', title: '习惯' } as const;
|
||||
case 'statistics':
|
||||
return { icon: 'chart.pie.fill', title: '健康' } as const;
|
||||
case 'personal':
|
||||
return { icon: 'person.fill', title: '个人' } as const;
|
||||
default:
|
||||
return { icon: 'circle', title: '' } as const;
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, title } = getIconAndTitle();
|
||||
const activeContentColor = colorTokens.tabIconSelected; // 使用专门为Tab定义的选中颜色
|
||||
const inactiveContentColor = colorTokens.tabIconDefault;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 6,
|
||||
marginVertical: 10,
|
||||
borderRadius: 25,
|
||||
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
|
||||
paddingHorizontal: isSelected ? 16 : 10,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol
|
||||
size={22}
|
||||
name={icon as any}
|
||||
color={isSelected ? activeContentColor : inactiveContentColor}
|
||||
/>
|
||||
{isSelected && !!title && (
|
||||
<Text
|
||||
style={{
|
||||
color: activeContentColor,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
}}
|
||||
// 选中态下不限制行数,避免大屏布局下被裁剪成省略号
|
||||
numberOfLines={0 as any}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
tabBarStyle: {
|
||||
position: 'absolute',
|
||||
bottom: TAB_BAR_BOTTOM_OFFSET,
|
||||
height: TAB_BAR_HEIGHT,
|
||||
borderRadius: 34,
|
||||
backgroundColor: colorTokens.tabBarBackground,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 10,
|
||||
elevation: 5,
|
||||
paddingHorizontal: 10,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginHorizontal: 20,
|
||||
width: '90%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tabBarItemStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
height: TAB_BAR_HEIGHT,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
tabBarShowLabel: false,
|
||||
};
|
||||
}}>
|
||||
|
||||
<Tabs.Screen
|
||||
name="statistics"
|
||||
options={{
|
||||
title: '统计',
|
||||
tabBarIcon: ({ color }) => {
|
||||
const isStatisticsSelected = pathname === '/statistics';
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol size={22} name="chart.pie.fill" color={color} />
|
||||
{isStatisticsSelected && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
健康
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="explore"
|
||||
options={{
|
||||
title: '发现',
|
||||
href: null,
|
||||
tabBarIcon: ({ color }) => {
|
||||
const isHomeSelected = pathname === '/' || pathname === '/index';
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol size={22} name="magnifyingglass.circle.fill" color={color} />
|
||||
{isHomeSelected && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
发现
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="coach"
|
||||
options={{
|
||||
title: 'Seal',
|
||||
tabBarIcon: ({ color }) => {
|
||||
const isCoachSelected = pathname === '/coach';
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol size={22} name="person.3.fill" color={color} />
|
||||
{isCoachSelected && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Seal
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="goals"
|
||||
options={{
|
||||
title: '目标',
|
||||
tabBarIcon: ({ color }) => {
|
||||
const isGoalsSelected = pathname === '/goals';
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol size={22} name="flag.fill" color={color} />
|
||||
{isGoalsSelected && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
目标
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="personal"
|
||||
options={{
|
||||
title: '个人',
|
||||
tabBarIcon: ({ color }) => {
|
||||
const isPersonalSelected = pathname === '/personal';
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconSymbol size={22} name="person.fill" color={color} />
|
||||
{isPersonalSelected && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
个人
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
||||
<Tabs.Screen name="explore" options={{ title: '发现', href: null }} />
|
||||
<Tabs.Screen name="coach" options={{ title: 'AI', href: null }} />
|
||||
<Tabs.Screen name="goals" options={{ title: '习惯' }} />
|
||||
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user