自定义选项卡布局
编辑
学习如何使用无头选项卡组件在 Expo Router 中创建自定义选项卡布局。
在 SDK 52 及更高版本中实验性可用。
Expo Router 提供了一组组件,通过子模块 expo-router/ui 创建自定义选项卡布局。与 React Navigation 风格的 Tabs 不同,这些组件是无样式和灵活的,旨在允许您从头开始在项目中构建复杂的 UI 模式。
有关其他选项卡布局,请参见:
如果您想为选项卡栏实现原生外观和感觉,请查看原生选项卡。
如果您已经使用 React Navigation 的选项卡,请查看 JavaScript 选项卡。
自定义 Tabs 组件的构成
expo-router/ui 提供了四个组件来创建自定义选项卡布局:
| 组件 | 描述 |
|---|---|
Tabs | 包含选项卡的 <View> 的包装组件。 |
TabList | 包含 TabTrigger 组件列表的 <View>。 |
TabTrigger | 切换到指定选项卡的触发组件。它用于使用 href 属性和每个选项卡的 name 定义路由。 |
TabSlot | 用于渲染当前选中的选项卡的插槽。 |
自定义选项卡布局的最低结构将包含一个 TabList(其中包含每个选项卡的 TabTrigger 组件)和一个 TabSlot,所有这些都在 Tabs 组件中,如下所示:
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui'; import { Text } from 'react-native'; // 定义自定义选项卡导航器的布局 export default function Layout() { return ( <Tabs> <TabSlot /> <TabList> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> <TabTrigger name="article" href="/article"> <Text>Article</Text> </TabTrigger> </TabList> </Tabs> ); }
创建路由
TabList 包含选项卡导航器内所有可用的路由。它必须是 Tabs 的直接子项。每个路由由 TabList 中的 TabTrigger 定义。TabList 中的 TabTrigger 必须包含一个 name 和一个 href 属性。
通常,TabList 定义了可用的选项卡路由和选项卡的外观,每个 TabTrigger 的子项定义了每个选项卡按钮的外观。
注意:
name可以是任何string。这是用户定义的选项卡名称。
动态路由
允许动态路由,并可以通过 href 提供值。
_layout.tsx[slug].tsx触发器 <TabTrigger name="dynamic page" href="/hello-world" /> 将为 [slug].tsx 创建一个选项卡,参数为 { slug: 'hello-world' }。这种设置可以用于基于最终用户数据显示任意数量的选项卡,比如在应用中为每个用户配置文件显示一个单独的选项卡。
模糊路由
_layout.tsx(one,two)route.tsx共享组中的路由提供给 TabTrigger 的 href 值必须始终指向单一路由。在上述共享路由示例中,href /route 是不允许的,因为它可以引用 /(one)/route 或 /(two)/route。但是,指定路由组的 href 将有效(例如href="/(one)/route")。
嵌套路由
_layout.tsx(stack-one)_layout.tsx一个 <Stack> 布局(stack-two)_layout.tsx嵌套 <Stack> 布局route.tsxTabTrigger 可以链接到嵌套很深的路由。<TabTrigger name="route" href="/route" /> 将显示 (stack-one)/(stack-two)/route.tsx 路由。这个选项卡将由该路由的父导航器控制(即在 stack-two_layout.tsx 中的导航器)。这种导航与深层链接类似。
渲染路由
TabSlot 组件渲染当前路由。TabSlot 可以嵌套在 Tabs 中的其他组件内,但不能在 TabList 中。
<Tabs> <TabList> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> </TabList> {/* 自定义 `<TabSlot />` 的渲染方式。 */} <View> <View> <TabSlot /> </View> </View> </Tabs>
切换选项卡
可以通过 Link 或使用命令式 API 切换选项卡。但是,这些 API 将始终执行导航操作(它们将切换选项卡并可能更改 URL)。要切换选项卡而不执行任何导航,应使用 TabTrigger。TabTrigger 是一个无样式的 <View>,当按下时会切换选项卡,就像文本和组件可以用 Link 包裹以使其成为可按压的导航元素一样。
重置导航
可以使用来自 TabTrigger 的 reset 属性控制选项卡何时重置其导航状态。选项有 always, onLongPress 和 never。这对于嵌套在选项卡内的堆栈导航器特别有用。例如,<TabTrigger name="home" reset="always" /> 将使用户返回到选项卡嵌套堆栈导航器内的索引路由。
TabTrigger
TabTrigger 用于切换选项卡,但也具有定义作为选项卡可用的路由的双重角色。
在 TabList 内
当 TabTrigger 作为 TabList 的子项使用时,定义了选项卡导航器内可用的路由。这些 TabTrigger 需要同时包含 name 和 href 属性,因为它们定义了该选项卡的 URL 和可用于引用该选项卡的自定义名称。如果 TabTrigger 组件还包含文本或其他组件作为子项,则这些也将作为选项卡按钮呈现。但是,您可以在 TabList 内定义没有任何 UI 的 TabTrigger,然后可以由在 TabList 外的 TabTrigger 调用。
在 TabList 外
可以在 TabList 外部定义额外的 TabTrigger,使您能够执行与在 TabList 定义的 TabTrigger 相同的操作。在这种情况下,TabTrigger 将没有 href 属性。相反,它将执行与主 TabTrigger 相同的操作,具有相同的 name 属性。这使您能够创建可以切换选项卡并与当前导航状态无关的组件。请注意所有 TabTrigger 至少需要是 Tabs 组件的后代,否则将被视为在选项卡导航器外并无法调用它。
自定义外观
除了 TabTrigger 以 <Pressable> 渲染外,所有组件都是作为无样式的 <View> 渲染。这使您可以提供自定义的 style 属性来美化其外观。给 TabList 样式设置类似于在 React Navigation 中自定义选项卡栏,给 TabTrigger 样式设置影响选项卡按钮的外观。
如果您需要改变组件的结构,您可以通过使用 asChild 属性重写其底层组件。该组件将充当插槽,并将其属性转发给其直接子项。
<Tabs> <TabSlot /> <TabList asChild> {/* 渲染自定义 TabList */} <CustomTabList> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> </CustomTabList> </TabList> </Tabs>
<Tabs> <TabSlot /> <TabList asChild> <TabTrigger name="home" href="/" asChild> {/* 渲染自定义按钮 */} <CustomButton> <Text>Home</Text> </CustomButton> </TabTrigger> </TabList> </Tabs>
多个选项卡栏
TabList 是 Tabs 的配置和默认外观,但这不是渲染选项卡栏的唯一方式。通过隐藏 TabList,您可以使用 TabTrigger 构建自定义选项卡栏。
<Tabs> <TabSlot /> {/* 自定义选项卡栏 */} <View> <View> <TabTrigger name="home"> <Text>Home</Text> </TabTrigger> <TabTrigger name="article"> <Text>article</Text> </TabTrigger> </View> </View> <TabList style={{ display: 'none' }}> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> <TabTrigger name="article" href="/article"> <Text>article</Text> </TabTrigger> </TabList> </Tabs>
TabTrigger 将转发一个 isFocused 属性,因此您可以创建一个单独的选项卡按钮组件,该组件对聚焦状态做出反应。
import FontAwesome from '@expo/vector-icons/FontAwesome'; import { TabTriggerSlotProps } from 'expo-router/ui'; import { ComponentProps, Ref } from 'react'; import { Text, Pressable, View } from 'react-native'; type Icon = ComponentProps<typeof FontAwesome>['name']; export type TabButtonProps = TabTriggerSlotProps & { icon?: Icon; ref: Ref<View>; }; export function TabButton({ icon, children, isFocused, ...props }: TabButtonProps) { return ( <Pressable {...props} style={[ { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexDirection: 'column', gap: 5, padding: 10, }, isFocused ? { backgroundColor: 'white' } : undefined, ]}> <FontAwesome name={icon} /> <Text style={[{ fontSize: 16 }, isFocused ? { color: 'white' } : undefined]}>{children}</Text> </Pressable> ); }
Expo SDK 52 / React 18 及更早版本
在 Expo SDK 52 及更早版本(React 18)中,使用遗留的 forwardRef 函数来访问 ref 句柄。
- import { ComponentProps, Ref } from 'react'; + import { ComponentProps, Ref, forwardRef } from 'react'; - export function TabButton({ ref }) { + export const TabButton = forwardRef((props: TabButtonProps, ref: Ref<View>) => {
钩子
所有组件也都有一个钩子版本,让您控制渲染树。有关可用钩子的完整列表,请参见 Router UI Reference。
使用钩子被视为该库的高级用法。对于大多数用例,使用 asChild 的组件应该能给您提供足够的控制权。
如果您正在开发自定义 <TabTrigger />,您可能还需要开发自定义 <TabList />,因为 <TabList /> 使用了 useTabsWithChildren(),这需要使用导出的 <TabTrigger /> 组件。
自定义选项卡屏幕的渲染方式
TabSlot 接受一个 renderFn 属性。此函数可用于覆盖屏幕的渲染方式,允许您实现高级功能,例如动画或持久化/卸载屏幕。有关更多信息,请参见 Router UI Reference。