135 lines
3.4 KiB
TypeScript
135 lines
3.4 KiB
TypeScript
import type { ViewStyle } from 'react-native'
|
|
import * as React from 'react'
|
|
import { Animated, Text, TouchableOpacity, View } from 'react-native'
|
|
import PagerView from 'react-native-pager-view'
|
|
|
|
export interface PagerRoute {
|
|
key: string
|
|
title: string
|
|
element?: React.ReactNode
|
|
}
|
|
|
|
interface PagerTabsProps {
|
|
routes: PagerRoute[]
|
|
left?: React.ReactNode
|
|
right?: React.ReactNode
|
|
selectedIndex?: number // 外部控制选中页
|
|
onChange?: (key: string) => void
|
|
indicatorWidth?: number
|
|
|
|
className?: string
|
|
style?: ViewStyle
|
|
}
|
|
|
|
export default function PagerTabs({
|
|
routes,
|
|
left,
|
|
right,
|
|
selectedIndex,
|
|
onChange,
|
|
indicatorWidth = 38,
|
|
className,
|
|
style,
|
|
}: PagerTabsProps) {
|
|
const pagerRef = React.useRef<PagerView>(null)
|
|
const [index, setIndex] = React.useState(selectedIndex ?? 0)
|
|
const [tabWidth, setTabWidth] = React.useState(0)
|
|
const scrollX = React.useRef(new Animated.Value(selectedIndex ?? 0)).current
|
|
|
|
// 外部 selectedIndex 更新时同步内部 index
|
|
React.useEffect(() => {
|
|
if (typeof selectedIndex === 'number' && selectedIndex !== index) {
|
|
setIndex(selectedIndex)
|
|
pagerRef.current?.setPage(selectedIndex)
|
|
}
|
|
}, [selectedIndex])
|
|
|
|
const onTabPress = (i: number) => {
|
|
pagerRef.current?.setPage(i)
|
|
}
|
|
|
|
const onPageScroll = (e: any) => {
|
|
const { position, offset } = e.nativeEvent
|
|
scrollX.setValue(position + offset)
|
|
}
|
|
|
|
const onPageSelected = (position: number) => {
|
|
setIndex(position)
|
|
if (onChange && routes[position]) {
|
|
onChange(routes[position].key)
|
|
}
|
|
}
|
|
|
|
const tabWidthPerTab = tabWidth / routes.length
|
|
const indicatorTranslateX = scrollX.interpolate({
|
|
inputRange: routes.map((_, i) => i),
|
|
outputRange: routes.map(
|
|
(_, i) => i * tabWidthPerTab + (tabWidthPerTab - indicatorWidth) / 2,
|
|
),
|
|
extrapolate: 'clamp',
|
|
})
|
|
|
|
return (
|
|
<View
|
|
className={`${className} flex-1`}
|
|
style={[style]}
|
|
>
|
|
{/* Header */}
|
|
<View className="flex flex-row items-center">
|
|
{left}
|
|
|
|
{/* Tabs */}
|
|
<View
|
|
className="flex-1 flex-row relative"
|
|
onLayout={(e) => {
|
|
const w = e.nativeEvent.layout.width
|
|
if (w !== tabWidth)
|
|
setTabWidth(w)
|
|
}}
|
|
>
|
|
{routes.map((r, i) => (
|
|
<TouchableOpacity
|
|
key={r.key}
|
|
className="flex-1 items-center justify-center py-2"
|
|
onPress={() => onTabPress(i)}
|
|
>
|
|
<Text
|
|
className={`text-lg ${index === i ? 'text-[#333] font-bold' : 'text-gray-500'}`}
|
|
>
|
|
{r.title}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
|
|
{tabWidth > 0 && (
|
|
<Animated.View
|
|
className="absolute bottom-0 h-0.5 bg-black"
|
|
style={{
|
|
width: indicatorWidth,
|
|
transform: [{ translateX: indicatorTranslateX }],
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
|
|
{right}
|
|
</View>
|
|
|
|
{/* Pager */}
|
|
<PagerView
|
|
ref={pagerRef}
|
|
style={{ flex: 1 }}
|
|
initialPage={selectedIndex ?? 0}
|
|
onPageScroll={onPageScroll}
|
|
onPageSelected={e => onPageSelected(e.nativeEvent.position)}
|
|
>
|
|
{routes.map(r => (
|
|
<View key={r.key} className="flex-1">
|
|
{r.element ?? null}
|
|
</View>
|
|
))}
|
|
</PagerView>
|
|
</View>
|
|
)
|
|
}
|