feat: 新增动画资源与庆祝效果,优化布局与标签页配置

This commit is contained in:
richarjiang
2025-09-03 15:03:26 +08:00
parent 8b6ef378d0
commit 951c02f644
14 changed files with 373 additions and 267 deletions

View File

@@ -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>
);
}