u
This commit is contained in:
1 parent
1d6bb2b7f5
commit
458f0cd6ac
55 files changed
+1052
-999
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import { usePathname } from 'expo-router'
|
||||
import { TabList, Tabs, TabSlot, TabTrigger } from 'expo-router/ui'
|
||||
import { icons } from 'lucide-react-native'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
export interface TabbarItem {
|
||||
icon: keyof typeof icons
|
||||
iconSize?: number
|
||||
activeIcon?: keyof typeof icons
|
||||
name: string
|
||||
href?: string
|
||||
onPress?: () => void
|
||||
}
|
||||
|
||||
interface AppTabbarProps {
|
||||
data: TabbarItem[]
|
||||
onTabPress?: (item: TabbarItem) => void
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
icon,
|
||||
activeIcon,
|
||||
iconSize = 24,
|
||||
isFocused = false,
|
||||
onPress,
|
||||
}: {
|
||||
icon: keyof typeof icons
|
||||
activeIcon?: keyof typeof icons
|
||||
iconSize?: number
|
||||
isFocused?: boolean
|
||||
onPress?: () => void
|
||||
}) {
|
||||
const activeColor = '#333'
|
||||
const inactiveColor = '#aaa'
|
||||
|
||||
const scale = useSharedValue(1)
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}))
|
||||
|
||||
const IconComponent = icons[isFocused && activeIcon ? activeIcon : icon]
|
||||
|
||||
const handlePress = () => {
|
||||
scale.value = withSequence(
|
||||
withTiming(0.9, { duration: 200 }),
|
||||
withTiming(1, { duration: 200 }),
|
||||
)
|
||||
onPress?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
className="flex-1 items-center justify-center h-full"
|
||||
>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<IconComponent
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
stroke={isFocused ? activeColor : inactiveColor}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AppTabbar({ data, onTabPress }: AppTabbarProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// 初始选中 Tab
|
||||
const defaultTab = data.find(item => item.href === pathname)?.name
|
||||
?? data.find(item => item.href)?.name
|
||||
?? data[0]?.name
|
||||
const [activeTab, setActiveTab] = useState<string>(defaultTab)
|
||||
|
||||
// 当 pathname 改变(路由跳转)时,同步高亮
|
||||
useEffect(() => {
|
||||
const matchTab = data.find(item => item.href === pathname)
|
||||
if (matchTab) {
|
||||
setActiveTab(matchTab.name)
|
||||
}
|
||||
// 如果没有匹配的路由,保持当前的 activeTab 不变
|
||||
// 这样无 href 的项就能保持选中状态
|
||||
}, [pathname, data])
|
||||
|
||||
const handleTabPress = (item: TabbarItem) => {
|
||||
setActiveTab(item.name)
|
||||
item.onPress?.()
|
||||
onTabPress?.(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<TabSlot />
|
||||
|
||||
<TabList asChild>
|
||||
<View className="flex-row bg-white h-12 items-center">
|
||||
{data.map((item) => {
|
||||
const isFocused = activeTab === item.name
|
||||
|
||||
if (item.href) {
|
||||
return (
|
||||
<TabTrigger key={item.name} name={item.name} href={item.href} asChild>
|
||||
<TabButton
|
||||
icon={item.icon}
|
||||
activeIcon={item.activeIcon}
|
||||
iconSize={item.iconSize}
|
||||
isFocused={isFocused}
|
||||
onPress={() => handleTabPress(item)}
|
||||
/>
|
||||
</TabTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabButton
|
||||
key={item.name}
|
||||
icon={item.icon}
|
||||
activeIcon={item.activeIcon}
|
||||
iconSize={item.iconSize}
|
||||
onPress={() => handleTabPress(item)}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user