122 lines
2.9 KiB
TypeScript
122 lines
2.9 KiB
TypeScript
import type {
|
|
LayoutChangeEvent,
|
|
} from 'react-native'
|
|
import React, { useEffect, useRef, useState } from 'react'
|
|
import {
|
|
ScrollView,
|
|
Text,
|
|
TouchableWithoutFeedback,
|
|
View,
|
|
} from 'react-native'
|
|
|
|
interface TabItem {
|
|
label: string
|
|
key: string | number
|
|
content?: React.ReactNode
|
|
}
|
|
|
|
interface TabProps {
|
|
data?: TabItem[]
|
|
defaultKey?: TabItem['key']
|
|
onChange?: (key: TabItem['key']) => void
|
|
}
|
|
|
|
export default function Tab({
|
|
data,
|
|
defaultKey,
|
|
onChange,
|
|
}: TabProps) {
|
|
if (!data || data.length === 0)
|
|
return null
|
|
|
|
/* ================= state ================= */
|
|
const initialKey = defaultKey ?? data[0].key
|
|
const [selectedKey, setSelectedKey] = useState(initialKey)
|
|
|
|
/* ================= refs ================= */
|
|
const scrollRef = useRef<ScrollView>(null)
|
|
|
|
// 每个 tab 的 layout 信息
|
|
const itemLayouts = useRef<
|
|
Record<
|
|
TabItem['key'],
|
|
{ x: number, width: number }
|
|
>
|
|
>({})
|
|
|
|
const containerWidth = useRef(0)
|
|
|
|
/* ================= handlers ================= */
|
|
const handlePress = (key: TabItem['key']) => {
|
|
setSelectedKey(key)
|
|
onChange?.(key)
|
|
scrollToCenter(key)
|
|
}
|
|
|
|
const scrollToCenter = (key: TabItem['key']) => {
|
|
const layout = itemLayouts.current[key]
|
|
if (!layout || !scrollRef.current)
|
|
return
|
|
|
|
const offset
|
|
= layout.x + layout.width / 2 - containerWidth.current / 2
|
|
|
|
scrollRef.current.scrollTo({
|
|
x: Math.max(0, offset),
|
|
animated: true,
|
|
})
|
|
}
|
|
|
|
/* ================= effects ================= */
|
|
useEffect(() => {
|
|
// 首次挂载后,让默认选中项居中
|
|
requestAnimationFrame(() => {
|
|
scrollToCenter(selectedKey)
|
|
})
|
|
}, [])
|
|
|
|
/* ================= render ================= */
|
|
return (
|
|
<ScrollView
|
|
ref={scrollRef}
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
onLayout={(e: LayoutChangeEvent) => {
|
|
containerWidth.current = e.nativeEvent.layout.width
|
|
}}
|
|
contentContainerStyle={{
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
{data.map((item) => {
|
|
const isActive = item.key === selectedKey
|
|
|
|
return (
|
|
<TouchableWithoutFeedback
|
|
key={item.key}
|
|
onPress={() => handlePress(item.key)}
|
|
>
|
|
<View
|
|
onLayout={(e) => {
|
|
const { x, width } = e.nativeEvent.layout
|
|
itemLayouts.current[item.key] = { x, width }
|
|
}}
|
|
className={
|
|
isActive
|
|
? 'items-center justify-center rounded-md px-4 py-2 bg-gray-800'
|
|
: 'items-center justify-center rounded-md px-4 py-2 bg-gray-200'
|
|
}
|
|
>
|
|
<Text className={isActive ? 'text-white' : 'text-gray-700'}>
|
|
{item.label}
|
|
</Text>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
)
|
|
})}
|
|
</ScrollView>
|
|
)
|
|
}
|