自定义选项卡布局

编辑

学习如何使用无头选项卡组件在 Expo Router 中创建自定义选项卡布局。


在 SDK 52 及更高版本中实验性可用。

Expo Router 提供了一组组件,通过子模块 expo-router/ui 创建自定义选项卡布局。与 React Navigation 风格的 Tabs 不同,这些组件是无样式和灵活的,旨在允许您从头开始在项目中构建复杂的 UI 模式。

有关其他选项卡布局,请参见:

原生选项卡

如果您想为选项卡栏实现原生外观和感觉,请查看原生选项卡。

JavaScript 选项卡

如果您已经使用 React Navigation 的选项卡,请查看 JavaScript 选项卡。

自定义 Tabs 组件的构成

expo-router/ui 提供了四个组件来创建自定义选项卡布局:

组件描述
Tabs包含选项卡的 <View> 的包装组件。
TabList包含 TabTrigger 组件列表的 <View>
TabTrigger切换到指定选项卡的触发组件。它用于使用 href 属性和每个选项卡的 name 定义路由。
TabSlot用于渲染当前选中的选项卡的插槽。

自定义选项卡布局的最低结构将包含一个 TabList(其中包含每个选项卡的 TabTrigger 组件)和一个 TabSlot,所有这些都在 Tabs 组件中,如下所示:

app/(tabs)/_layout.tsx
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共享组中的路由

提供给 TabTriggerhref 值必须始终指向单一路由。在上述共享路由示例中,href /route 是不允许的,因为它可以引用 /(one)/route/(two)/route。但是,指定路由组的 href 将有效(例如href="/(one)/route")。

嵌套路由

_layout.tsx
(stack-one)
_layout.tsx一个 <Stack> 布局
(stack-two)
  _layout.tsx嵌套 <Stack> 布局
  route.tsx

TabTrigger 可以链接到嵌套很深的路由。<TabTrigger name="route" href="/route" /> 将显示 (stack-one)/(stack-two)/route.tsx 路由。这个选项卡将由该路由的父导航器控制(即在 stack-two_layout.tsx 中的导航器)。这种导航与深层链接类似。

渲染路由

TabSlot 组件渲染当前路由。TabSlot 可以嵌套在 Tabs 中的其他组件内,但不能在 TabList 中。

app/_layout.tsx
<Tabs> <TabList> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> </TabList> {/* 自定义 `<TabSlot />` 的渲染方式。 */} <View> <View> <TabSlot /> </View> </View> </Tabs>

切换选项卡

可以通过 Link 或使用命令式 API 切换选项卡。但是,这些 API 将始终执行导航操作(它们将切换选项卡并可能更改 URL)。要切换选项卡而不执行任何导航,应使用 TabTriggerTabTrigger 是一个无样式的 <View>,当按下时会切换选项卡,就像文本和组件可以用 Link 包裹以使其成为可按压的导航元素一样。

重置导航

可以使用来自 TabTriggerreset 属性控制选项卡何时重置其导航状态。选项有 always, onLongPressnever。这对于嵌套在选项卡内的堆栈导航器特别有用。例如,<TabTrigger name="home" reset="always" /> 将使用户返回到选项卡嵌套堆栈导航器内的索引路由。

TabTrigger

TabTrigger 用于切换选项卡,但也具有定义作为选项卡可用的路由的双重角色。

在 TabList 内

TabTrigger 作为 TabList 的子项使用时,定义了选项卡导航器内可用的路由。这些 TabTrigger 需要同时包含 namehref 属性,因为它们定义了该选项卡的 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 属性重写其底层组件。该组件将充当插槽,并将其属性转发给其直接子项。

Custom TabList
<Tabs> <TabSlot /> <TabList asChild> {/* 渲染自定义 TabList */} <CustomTabList> <TabTrigger name="home" href="/"> <Text>Home</Text> </TabTrigger> </CustomTabList> </TabList> </Tabs>
Custom Button
<Tabs> <TabSlot /> <TabList asChild> <TabTrigger name="home" href="/" asChild> {/* 渲染自定义按钮 */} <CustomButton> <Text>Home</Text> </CustomButton> </TabTrigger> </TabList> </Tabs>

多个选项卡栏

TabListTabs 的配置和默认外观,但这不是渲染选项卡栏的唯一方式。通过隐藏 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 属性,因此您可以创建一个单独的选项卡按钮组件,该组件对聚焦状态做出反应。

TabButton.tsx
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

常见问题

我如何为相同路由创建多个选项卡?
_layout.tsx选项卡布局
(movie,tv)
[id].tsx

您应该将路由添加到共享组中并为每个组 group 创建单独的 TabTrigger

我如何隐藏选项卡?

不渲染 TabTrigger 将从您的应用中移除该选项卡(及其导航状态)。

我如何创建动画选项卡?

您可以向 TabSlot 提供自定义呈现器,以自定义其渲染屏幕的方式。您可以使用此方法检测屏幕何时聚焦并相应地进行动画处理。

我可以使用相对 href 吗?
directory
_layout.tsx本地路径名是 /directory
page.tsx路径名是 /directory/page
profile.tsx路径名是 /directory/profile

具有相对 href 的 TabTrigger 相对于 Tabs 渲染的本地路径名。与当前显示路由的正常相对 href 不同。例如,<TabTrigger href="./profile" /> 将解析为 /directory/profile,即使当前显示的是 /directory/page 路由。Expo 不建议使用相对 href。