feat: enhance TabLayout and HomeScreen with safe area insets and optimized refresh handling
This commit is contained in:
@@ -3,11 +3,13 @@ import React from 'react'
|
|||||||
import { StyleSheet, View, Text } from 'react-native'
|
import { StyleSheet, View, Text } from 'react-native'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
|
||||||
import { HomeIcon, MessageIcon, MyIcon, VideoIcon } from '@/components/icon'
|
import { HomeIcon, MessageIcon, MyIcon, VideoIcon } from '@/components/icon'
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -19,9 +21,9 @@ export default function TabLayout() {
|
|||||||
tabBarShowLabel: true,
|
tabBarShowLabel: true,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: '#1C1E22CC',
|
backgroundColor: '#1C1E22CC',
|
||||||
height: 83,
|
height: 84 + insets.bottom,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 4,
|
paddingBottom: insets.bottom,
|
||||||
paddingLeft: 4,
|
paddingLeft: 4,
|
||||||
paddingRight: 4,
|
paddingRight: 4,
|
||||||
borderTopWidth: 0,
|
borderTopWidth: 0,
|
||||||
|
|||||||
@@ -115,15 +115,13 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
// 下拉刷新处理
|
// 下拉刷新处理 - 只刷新当前分类的模板
|
||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
|
if (!selectedCategoryId) return
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
await Promise.all([
|
await loadTemplates({ categoryId: selectedCategoryId })
|
||||||
loadCategories(),
|
|
||||||
selectedCategoryId ? loadTemplates({ categoryId: selectedCategoryId }) : Promise.resolve(),
|
|
||||||
])
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}, [selectedCategoryId, loadCategories, loadTemplates])
|
}, [selectedCategoryId, loadTemplates])
|
||||||
|
|
||||||
// 加载更多处理
|
// 加载更多处理
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
@@ -289,7 +287,7 @@ export default function HomeScreen() {
|
|||||||
numColumns={NUM_COLUMNS}
|
numColumns={NUM_COLUMNS}
|
||||||
columnWrapperStyle={styles.columnWrapper}
|
columnWrapperStyle={styles.columnWrapper}
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={[styles.scrollContent, { paddingBottom: 60 + insets.bottom + 20 }]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScroll={(e) => handleScroll(e.nativeEvent.contentOffset.y)}
|
onScroll={(e) => handleScroll(e.nativeEvent.contentOffset.y)}
|
||||||
scrollEventThrottle={Platform.OS === 'ios' ? 16 : 50}
|
scrollEventThrottle={Platform.OS === 'ios' ? 16 : 50}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useRef, useEffect, useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -6,9 +6,15 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
|
Dimensions,
|
||||||
|
LayoutChangeEvent,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { DownArrowIcon } from '@/components/icon'
|
import { DownArrowIcon } from '@/components/icon'
|
||||||
|
|
||||||
|
const SCREEN_WIDTH = Dimensions.get('window').width
|
||||||
|
const ARROW_WIDTH = 48 // 箭头按钮宽度 (padding 8 * 2 + icon ~24 + marginLeft 8)
|
||||||
|
const HORIZONTAL_PADDING = 16
|
||||||
|
|
||||||
interface TabNavigationProps {
|
interface TabNavigationProps {
|
||||||
tabs: string[]
|
tabs: string[]
|
||||||
activeIndex: number
|
activeIndex: number
|
||||||
@@ -30,6 +36,42 @@ export function TabNavigation({
|
|||||||
wrapperStyle,
|
wrapperStyle,
|
||||||
onLayout,
|
onLayout,
|
||||||
}: TabNavigationProps): JSX.Element {
|
}: TabNavigationProps): JSX.Element {
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null)
|
||||||
|
const tabLayouts = useRef<{ x: number; width: number }[]>([])
|
||||||
|
|
||||||
|
// 计算可用的滚动区域宽度
|
||||||
|
const scrollAreaWidth = SCREEN_WIDTH - HORIZONTAL_PADDING * 2 - (showArrow ? ARROW_WIDTH : 0)
|
||||||
|
|
||||||
|
// 滚动到激活的 tab 使其居中
|
||||||
|
const scrollToActiveTab = useCallback((index: number) => {
|
||||||
|
const layout = tabLayouts.current[index]
|
||||||
|
if (!layout || !scrollViewRef.current) return
|
||||||
|
|
||||||
|
// 计算让 tab 居中需要的滚动位置
|
||||||
|
const tabCenter = layout.x + layout.width / 2
|
||||||
|
const scrollX = tabCenter - scrollAreaWidth / 2
|
||||||
|
|
||||||
|
scrollViewRef.current.scrollTo({
|
||||||
|
x: Math.max(0, scrollX),
|
||||||
|
animated: true,
|
||||||
|
})
|
||||||
|
}, [scrollAreaWidth])
|
||||||
|
|
||||||
|
// 当 activeIndex 变化时滚动
|
||||||
|
useEffect(() => {
|
||||||
|
// 延迟执行确保布局已完成
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
scrollToActiveTab(activeIndex)
|
||||||
|
}, 50)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [activeIndex, scrollToActiveTab])
|
||||||
|
|
||||||
|
// 记录每个 tab 的布局
|
||||||
|
const handleTabLayout = useCallback((index: number, event: LayoutChangeEvent) => {
|
||||||
|
const { x, width } = event.nativeEvent.layout
|
||||||
|
tabLayouts.current[index] = { x, width }
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
testID="tab-navigation"
|
testID="tab-navigation"
|
||||||
@@ -45,6 +87,7 @@ export function TabNavigation({
|
|||||||
>
|
>
|
||||||
<View style={styles.tabsContainer}>
|
<View style={styles.tabsContainer}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.tabsScrollContent}
|
contentContainerStyle={styles.tabsScrollContent}
|
||||||
@@ -55,6 +98,7 @@ export function TabNavigation({
|
|||||||
testID={`tab-${index}`}
|
testID={`tab-${index}`}
|
||||||
style={[styles.tab, activeIndex === index && styles.activeTab]}
|
style={[styles.tab, activeIndex === index && styles.activeTab]}
|
||||||
onPress={() => onTabPress(index)}
|
onPress={() => onTabPress(index)}
|
||||||
|
onLayout={(e) => handleTabLayout(index, e)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
Reference in New Issue
Block a user