feat: Enhance mobile and tablet support with responsive layout adjustments and new navigation components

This commit is contained in:
zimplexing
2025-08-01 16:36:28 +08:00
parent 942703509e
commit 9e9e4597cc
35 changed files with 4082 additions and 634 deletions

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Text, Platform } from 'react-native';
import { useRouter, usePathname } from 'expo-router';
import { Home, Search, Heart, Settings, Tv } from 'lucide-react-native';
import { Colors } from '@/constants/Colors';
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
import { DeviceUtils } from '@/utils/DeviceUtils';
interface TabItem {
key: string;
label: string;
icon: React.ComponentType<any>;
route: string;
}
const tabs: TabItem[] = [
{ key: 'home', label: '首页', icon: Home, route: '/' },
{ key: 'search', label: '搜索', icon: Search, route: '/search' },
{ key: 'live', label: '直播', icon: Tv, route: '/live' },
{ key: 'favorites', label: '收藏', icon: Heart, route: '/favorites' },
{ key: 'settings', label: '设置', icon: Settings, route: '/settings' },
];
const MobileBottomTabNavigator: React.FC = () => {
const router = useRouter();
const pathname = usePathname();
const { spacing } = useResponsiveLayout();
const handleTabPress = (route: string) => {
if (route === '/') {
router.push('/');
} else {
router.push(route as any);
}
};
const isTabActive = (route: string) => {
if (route === '/' && pathname === '/') return true;
if (route !== '/' && pathname === route) return true;
return false;
};
const dynamicStyles = createStyles(spacing);
return (
<View style={dynamicStyles.container}>
{tabs.map((tab) => {
const isActive = isTabActive(tab.route);
const IconComponent = tab.icon;
return (
<TouchableOpacity
key={tab.key}
style={[dynamicStyles.tab, isActive && dynamicStyles.activeTab]}
onPress={() => handleTabPress(tab.route)}
activeOpacity={0.7}
>
<IconComponent
size={20}
color={isActive ? Colors.dark.primary : '#888'}
strokeWidth={isActive ? 2.5 : 2}
/>
<Text style={[
dynamicStyles.tabLabel,
isActive && dynamicStyles.activeTabLabel
]}>
{tab.label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
};
const createStyles = (spacing: number) => {
const minTouchTarget = DeviceUtils.getMinTouchTargetSize();
return StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: '#1c1c1e',
borderTopWidth: 1,
borderTopColor: '#333',
paddingTop: spacing / 2,
paddingBottom: Platform.OS === 'ios' ? spacing * 2 : spacing,
paddingHorizontal: spacing,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: -2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 10,
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
minHeight: minTouchTarget,
paddingVertical: spacing / 2,
borderRadius: 8,
},
activeTab: {
backgroundColor: 'rgba(64, 156, 255, 0.1)',
},
tabLabel: {
fontSize: 11,
color: '#888',
marginTop: 2,
fontWeight: '500',
},
activeTabLabel: {
color: Colors.dark.primary,
fontWeight: '600',
},
});
};
export default MobileBottomTabNavigator;

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, StatusBar, Platform } from 'react-native';
import { useRouter } from 'expo-router';
import { ArrowLeft } from 'lucide-react-native';
import { ThemedText } from '@/components/ThemedText';
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
import { DeviceUtils } from '@/utils/DeviceUtils';
interface ResponsiveHeaderProps {
title?: string;
showBackButton?: boolean;
rightComponent?: React.ReactNode;
onBackPress?: () => void;
}
const ResponsiveHeader: React.FC<ResponsiveHeaderProps> = ({
title,
showBackButton = false,
rightComponent,
onBackPress,
}) => {
const router = useRouter();
const { deviceType, spacing } = useResponsiveLayout();
// TV端不显示Header使用现有的页面内导航
if (deviceType === 'tv') {
return null;
}
const handleBackPress = () => {
if (onBackPress) {
onBackPress();
} else if (router.canGoBack()) {
router.back();
}
};
const dynamicStyles = createStyles(spacing, deviceType);
return (
<>
{Platform.OS === 'android' && <StatusBar backgroundColor="#1c1c1e" barStyle="light-content" />}
<View style={dynamicStyles.container}>
<View style={dynamicStyles.content}>
{/* 左侧区域 */}
<View style={dynamicStyles.leftSection}>
{showBackButton && (
<TouchableOpacity
onPress={handleBackPress}
style={dynamicStyles.backButton}
activeOpacity={0.7}
>
<ArrowLeft size={20} color="#fff" strokeWidth={2} />
</TouchableOpacity>
)}
</View>
{/* 中间标题区域 */}
<View style={dynamicStyles.centerSection}>
{title && (
<ThemedText style={dynamicStyles.title} numberOfLines={1}>
{title}
</ThemedText>
)}
</View>
{/* 右侧区域 */}
<View style={dynamicStyles.rightSection}>
{rightComponent}
</View>
</View>
</View>
</>
);
};
const createStyles = (spacing: number, deviceType: string) => {
const minTouchTarget = DeviceUtils.getMinTouchTargetSize();
const statusBarHeight = Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24;
return StyleSheet.create({
container: {
backgroundColor: '#1c1c1e',
paddingTop: statusBarHeight,
borderBottomWidth: 1,
borderBottomColor: '#333',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 5,
},
content: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing,
paddingVertical: spacing * 0.75,
minHeight: minTouchTarget + spacing,
},
leftSection: {
width: minTouchTarget + spacing,
justifyContent: 'flex-start',
alignItems: 'flex-start',
},
centerSection: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
rightSection: {
width: minTouchTarget + spacing,
justifyContent: 'flex-end',
alignItems: 'flex-end',
flexDirection: 'row',
},
backButton: {
width: minTouchTarget,
height: minTouchTarget,
justifyContent: 'center',
alignItems: 'center',
borderRadius: minTouchTarget / 2,
},
title: {
fontSize: DeviceUtils.getOptimalFontSize(deviceType === 'mobile' ? 18 : 20),
fontWeight: '600',
color: '#fff',
},
});
};
export default ResponsiveHeader;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
import MobileBottomTabNavigator from './MobileBottomTabNavigator';
import TabletSidebarNavigator from './TabletSidebarNavigator';
interface ResponsiveNavigationProps {
children: React.ReactNode;
}
const ResponsiveNavigation: React.FC<ResponsiveNavigationProps> = ({ children }) => {
const { deviceType } = useResponsiveLayout();
switch (deviceType) {
case 'mobile':
return (
<View style={styles.container}>
<View style={styles.content}>
{children}
</View>
<MobileBottomTabNavigator />
</View>
);
case 'tablet':
return (
<TabletSidebarNavigator>
{children}
</TabletSidebarNavigator>
);
case 'tv':
default:
// TV端保持原有的Stack导航不需要额外的导航容器
return <>{children}</>;
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
},
});
export default ResponsiveNavigation;

View File

@@ -0,0 +1,240 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Text, ScrollView } from 'react-native';
import { useRouter, usePathname } from 'expo-router';
import { Home, Search, Heart, Settings, Tv, Menu, X } from 'lucide-react-native';
import { Colors } from '@/constants/Colors';
import { useResponsiveLayout } from '@/hooks/useResponsiveLayout';
import { DeviceUtils } from '@/utils/DeviceUtils';
import { ThemedText } from '@/components/ThemedText';
interface SidebarItem {
key: string;
label: string;
icon: React.ComponentType<any>;
route: string;
section?: string;
}
const sidebarItems: SidebarItem[] = [
{ key: 'home', label: '首页', icon: Home, route: '/', section: 'main' },
{ key: 'search', label: '搜索', icon: Search, route: '/search', section: 'main' },
{ key: 'live', label: '直播', icon: Tv, route: '/live', section: 'main' },
{ key: 'favorites', label: '收藏', icon: Heart, route: '/favorites', section: 'user' },
{ key: 'settings', label: '设置', icon: Settings, route: '/settings', section: 'user' },
];
interface TabletSidebarNavigatorProps {
children: React.ReactNode;
collapsed?: boolean;
onToggleCollapse?: (collapsed: boolean) => void;
}
const TabletSidebarNavigator: React.FC<TabletSidebarNavigatorProps> = ({
children,
collapsed: controlledCollapsed,
onToggleCollapse,
}) => {
const router = useRouter();
const pathname = usePathname();
const { spacing, isPortrait } = useResponsiveLayout();
const [internalCollapsed, setInternalCollapsed] = useState(false);
// 使用外部控制的collapsed状态如果没有则使用内部状态
const collapsed = controlledCollapsed !== undefined ? controlledCollapsed : internalCollapsed;
const handleToggleCollapse = () => {
if (onToggleCollapse) {
onToggleCollapse(!collapsed);
} else {
setInternalCollapsed(!collapsed);
}
};
const handleItemPress = (route: string) => {
if (route === '/') {
router.push('/');
} else {
router.push(route as any);
}
// 在竖屏模式下,导航后自动折叠侧边栏
if (isPortrait && !controlledCollapsed) {
setInternalCollapsed(true);
}
};
const isItemActive = (route: string) => {
if (route === '/' && pathname === '/') return true;
if (route !== '/' && pathname === route) return true;
return false;
};
const sidebarWidth = collapsed ? 60 : 200;
const dynamicStyles = createStyles(spacing, sidebarWidth, isPortrait);
const renderSidebarItems = () => {
const sections = ['main', 'user'];
return sections.map((section) => {
const sectionItems = sidebarItems.filter(item => item.section === section);
return (
<View key={section} style={dynamicStyles.section}>
{!collapsed && (
<ThemedText style={dynamicStyles.sectionTitle}>
{section === 'main' ? '主要功能' : '用户'}
</ThemedText>
)}
{sectionItems.map((item) => {
const isActive = isItemActive(item.route);
const IconComponent = item.icon;
return (
<TouchableOpacity
key={item.key}
style={[dynamicStyles.sidebarItem, isActive && dynamicStyles.activeSidebarItem]}
onPress={() => handleItemPress(item.route)}
activeOpacity={0.7}
>
<IconComponent
size={20}
color={isActive ? Colors.dark.primary : '#ccc'}
strokeWidth={isActive ? 2.5 : 2}
/>
{!collapsed && (
<Text style={[
dynamicStyles.sidebarItemLabel,
isActive && dynamicStyles.activeSidebarItemLabel
]}>
{item.label}
</Text>
)}
</TouchableOpacity>
);
})}
</View>
);
});
};
return (
<View style={dynamicStyles.container}>
{/* 侧边栏 */}
<View style={[dynamicStyles.sidebar, collapsed && dynamicStyles.collapsedSidebar]}>
{/* 侧边栏头部 */}
<View style={dynamicStyles.sidebarHeader}>
<TouchableOpacity
onPress={handleToggleCollapse}
style={dynamicStyles.toggleButton}
activeOpacity={0.7}
>
{collapsed ? (
<Menu size={20} color="#ccc" />
) : (
<X size={20} color="#ccc" />
)}
</TouchableOpacity>
{!collapsed && (
<ThemedText style={dynamicStyles.appTitle}>OrionTV</ThemedText>
)}
</View>
{/* 侧边栏内容 */}
<ScrollView style={dynamicStyles.sidebarContent} showsVerticalScrollIndicator={false}>
{renderSidebarItems()}
</ScrollView>
</View>
{/* 主内容区域 */}
<View style={dynamicStyles.content}>
{children}
</View>
</View>
);
};
const createStyles = (spacing: number, sidebarWidth: number, isPortrait: boolean) => {
const minTouchTarget = DeviceUtils.getMinTouchTargetSize();
return StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
},
sidebar: {
width: sidebarWidth,
backgroundColor: '#1c1c1e',
borderRightWidth: 1,
borderRightColor: '#333',
zIndex: isPortrait ? 1000 : 1, // 在竖屏时提高层级
},
collapsedSidebar: {
width: 60,
},
sidebarHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing,
paddingVertical: spacing * 1.5,
borderBottomWidth: 1,
borderBottomColor: '#333',
},
toggleButton: {
width: minTouchTarget,
height: minTouchTarget,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
},
appTitle: {
fontSize: 18,
fontWeight: 'bold',
marginLeft: spacing,
color: Colors.dark.primary,
},
sidebarContent: {
flex: 1,
paddingTop: spacing,
},
section: {
marginBottom: spacing * 1.5,
},
sectionTitle: {
fontSize: 12,
color: '#888',
fontWeight: '600',
textTransform: 'uppercase',
marginBottom: spacing / 2,
marginHorizontal: spacing,
},
sidebarItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing,
paddingVertical: spacing * 0.75,
marginHorizontal: spacing / 2,
borderRadius: 8,
minHeight: minTouchTarget,
},
activeSidebarItem: {
backgroundColor: 'rgba(64, 156, 255, 0.15)',
},
sidebarItemLabel: {
fontSize: 14,
color: '#ccc',
marginLeft: spacing,
fontWeight: '500',
},
activeSidebarItemLabel: {
color: Colors.dark.primary,
fontWeight: '600',
},
content: {
flex: 1,
backgroundColor: '#000',
},
});
};
export default TabletSidebarNavigator;