堆栈工具栏

编辑页面

了解如何在使用 Expo Router 的堆栈导航中使用原生工具栏。


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

重要 Stack.Toolbar 是一个 alpha API,适用于 Expo SDK 56 及更高版本的 Android,以及 Expo SDK 55 及更高版本的 iOS。该 API 可能会发生破坏性更改。

Stack.Toolbar 允许你在 Android 和 iOS 的 Stack 屏幕中添加原生工具栏项。你可以将按钮、菜单和自定义视图放置在标题栏(左侧或右侧)或底部工具栏中。

添加标题栏按钮

Stack.Toolbar 中使用 Stack.Toolbar.Button,并设置 placement="right"placement="left",即可向导航标题栏添加按钮。这对于收藏、分享或编辑内容等操作非常有用。

src/app/notes/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { View, Text, Alert } from 'react-native'; export default function NoteScreen() { const [isFavorite, setIsFavorite] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button // 将此处替换为你自己的图标 icon={isFavorite ? require('./assets/star-filled.png') : require('./assets/star.png')} onPress={() => setIsFavorite(!isFavorite)} /> <Stack.Toolbar.Button icon={require('./assets/share.png')} onPress={() => Alert.alert('Share')} /> </Stack.Toolbar> <Stack.Toolbar placement="left"> <Stack.Toolbar.Button icon={require('./assets/sidebar.png')} onPress={() => Alert.alert('Sidebar')} /> </Stack.Toolbar> <View style={{ flex: 1, padding: 16 }}> <Text>Note content...</Text> </View> </> ); }
src/app/notes/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { View, Text, Alert } from 'react-native'; export default function NoteScreen() { const [isFavorite, setIsFavorite] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button icon={isFavorite ? 'star.fill' : 'star'} onPress={() => setIsFavorite(!isFavorite)} /> <Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => Alert.alert('分享')} /> </Stack.Toolbar> <Stack.Toolbar placement="left"> <Stack.Toolbar.Button icon="sidebar.left" onPress={() => Alert.alert('侧边栏')} /> </Stack.Toolbar> <View style={{ flex: 1, padding: 16 }}> <Text>笔记内容...</Text> </View> </> ); }

图标

工具栏按钮支持 SF Symbols(仅限 iOS)和自定义图片(Android 和 iOS)。

SF Symbols(仅限 iOS)

在 iOS 上添加图标最简单的方法是使用 SF Symbols,这是 Apple 内置的图标库。直接将符号名称传递给 icon 属性:

<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} /> <Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => {}} /> <Stack.Toolbar.Menu icon="ellipsis.circle">{/* ... */}</Stack.Toolbar.Menu>

你可以在 Apple 的 SF Symbols 应用中浏览可用符号。

信息 SF Symbols 是仅限 iOS 的功能。

Material Symbols(仅限 Android)

Android 推荐使用 @expo/material-symbols 库来提供图标。它将 Google 的 Material Symbols 作为独立的资源子路径提供,因此 Metro 只会打包你实际导入的图标。

Terminal
npx expo install @expo/material-symbols

直接从各自的子路径导入任意图标,并将其传递给 icon 属性:

import Star from '@expo/material-symbols/star.xml'; import Share from '@expo/material-symbols/share.xml'; import MoreVert from '@expo/material-symbols/more_vert.xml'; <Stack.Toolbar.Button icon={Star} onPress={() => {}} /> <Stack.Toolbar.Button icon={Share} onPress={() => {}} /> <Stack.Toolbar.Menu icon={MoreVert}>{/* ... */}</Stack.Toolbar.Menu>

向量可绘制对象默认会使用工具栏的 tint 颜色着色。传递 iconRenderingMode="original" 可保留源颜色。

信息 Material Symbols XML 可绘制对象是仅限 Android 的功能。在 iOS 上,请改用 SF Symbols。

在 Android 和 iOS 上使用相同图标

Stack.Toolbar.Buttonicon 属性既接受 ImageSourcePropType(Android),也接受 SF Symbol 名称(iOS)。如果要在两个平台上使用同一个组件,请根据 process.env.EXPO_OS 分支,并传入适合平台的值。Metro 会在构建时将 process.env.EXPO_OS 替换为字符串字面量,然后对不匹配当前平台的分支进行 tree-shaking——因此 Material Symbols XML 可绘制对象不会出现在 iOS 包中,SF Symbol 名称也不会出现在 Android 包中:

import Star from '@expo/material-symbols/star.xml'; <Stack.Toolbar.Button icon={process.env.EXPO_OS === 'ios' ? 'star.fill' : Star} onPress={() => {}} />;

自定义图片

你也可以使用自定义图片。传递它们的 API 因平台而异:

将图片传递给 icon 属性:

import { Stack } from 'expo-router'; export default function Page() { return ( <> <Stack.Toolbar> <Stack.Toolbar.Button icon={require('./assets/expo.png')} onPress={() => {}} /> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

默认情况下,来自图片源的图标会使用工具栏的着色颜色进行着色(iconRenderingMode 默认值为 'template')。传递 iconRenderingMode="original" 可保留源图片的原始颜色,这对于多色图标很有用:

import { Stack } from 'expo-router'; export default function Page() { return ( <> <Stack.Toolbar> <Stack.Toolbar.Button icon={require('./assets/expo.png')} iconRenderingMode="original" onPress={() => {}} /> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

iOS 根据位置使用两种不同的 API:对于标题栏工具栏,直接将图片源传递给 icon;对于底部工具栏,则使用 useImageimage 属性。

信息 在标题栏位置的子菜单(Stack.Toolbar.Menu)中使用自定义图片需要 react-native-screens 4.24.0 或更高版本。SDK 55 自带的是 ~4.23.0,因此你需要手动安装 react-native-screens@~4.24.0 才能使用此功能。SDK 56 默认捆绑了兼容版本。

import { Stack } from 'expo-router'; export default function Page() { return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button icon={require('./assets/expo.png')} onPress={() => {}} /> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

在底部工具栏中,使用 expo-image 中的 useImage hook,并将结果传递给 image 属性:

import { Stack } from 'expo-router'; import { useImage } from 'expo-image'; export default function Page() { const customIcon = useImage('https://simpleicons.org/icons/expo.svg', { maxWidth: 24, maxHeight: 24, }); return ( <> <Stack.Toolbar> <Stack.Toolbar.Button image={customIcon} onPress={() => {}} /> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

信息 用于底部工具栏自定义图片的 useImageimage 属性模式仅限 iOS,并且是一个临时 API,未来版本中可能会更改。

构建操作菜单

对于有多个操作的屏幕,使用 Stack.Toolbar.Menu 将它们分组到一个下拉菜单中:

信息 某些 Stack.Toolbar.MenuStack.Toolbar.MenuAction 属性仅限 iOS。请参阅 API 参考文档了解每个属性的平台可用性。

src/app/mail/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function EmailScreen() { const [isArchived, setIsArchived] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Menu icon={require('./assets/menu.png')}> <Stack.Toolbar.MenuAction icon={require('./assets/reply.png')} onPress={() => Alert.alert('Reply')}> Reply </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon={require('./assets/forward.png')} onPress={() => Alert.alert('Forward')}> Forward </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon={isArchived ? require('./assets/unarchive.png') : require('./assets/archive.png')} isOn={isArchived} onPress={() => setIsArchived(!isArchived)}> {isArchived ? 'Unarchive' : 'Archive'} </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon={require('./assets/trash.png')} destructive onPress={() => Alert.alert('Delete')}> Delete </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* 邮件内容 */} </> ); }
src/app/mail/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function EmailScreen() { const [isArchived, setIsArchived] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Menu icon="ellipsis.circle"> <Stack.Toolbar.MenuAction icon="arrowshape.turn.up.left" onPress={() => Alert.alert('回复')}> 回复 </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon="arrowshape.turn.up.right" onPress={() => Alert.alert('转发')}> 转发 </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon={isArchived ? 'tray.full' : 'archivebox'} isOn={isArchived} onPress={() => setIsArchived(!isArchived)}> {isArchived ? '取消归档' : '归档'} </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon="trash" destructive onPress={() => Alert.alert('删除')}> 删除 </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* 邮件内容 */} </> ); }

嵌套子菜单

对于更复杂的菜单,可以在另一个菜单中嵌套 Stack.Toolbar.Menu。使用 inline 属性可以直接显示子菜单项,而不会折叠:

import { useState } from 'react'; import { Stack } from 'expo-router'; export default function EmailScreen() { const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('name'); const [showHiddenFiles, setShowHiddenFiles] = useState(false); return ( <> <Stack.Toolbar> <Stack.Toolbar.Menu icon={require('./assets/menu.png')}> {/* 内联子菜单 - 选项会直接显示在菜单中 */} <Stack.Toolbar.Menu inline title="Sort By"> <Stack.Toolbar.MenuAction isOn={sortBy === 'name'} onPress={() => setSortBy('name')}> Name </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'date'} onPress={() => setSortBy('date')}> Date </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'size'} onPress={() => setSortBy('size')}> Size </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> {/* 嵌套子菜单 - 作为单独菜单打开 */} <Stack.Toolbar.Menu title="Preferences"> <Stack.Toolbar.MenuAction isOn={showHiddenFiles} onPress={() => setShowHiddenFiles(!showHiddenFiles)}> Show Hidden Files </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* 邮件内容 */} </> ); }
import { useState } from 'react'; import { Stack } from 'expo-router'; export default function EmailScreen() { const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('name'); const [showHiddenFiles, setShowHiddenFiles] = useState(false); return ( <> <Stack.Toolbar> <Stack.Toolbar.Menu icon="ellipsis.circle"> {/* 内联子菜单 - 选项会直接显示在菜单中 */} <Stack.Toolbar.Menu inline title="排序方式"> <Stack.Toolbar.MenuAction isOn={sortBy === 'name'} onPress={() => setSortBy('name')}> 名称 </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'date'} onPress={() => setSortBy('date')}> 日期 </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'size'} onPress={() => setSortBy('size')}> 大小 </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> {/* 嵌套子菜单 - 作为单独菜单打开 */} <Stack.Toolbar.Menu title="偏好设置"> <Stack.Toolbar.MenuAction isOn={showHiddenFiles} onPress={() => setShowHiddenFiles(!showHiddenFiles)}> 显示隐藏文件 </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* 邮件内容 */} </> ); }

使用底部工具栏

底部工具栏在 iOS 上通常用于主要屏幕操作,例如 Photos 和 Mail 应用中的工具栏。要添加一个工具栏,请在不传入 placement 属性的情况下使用 Stack.Toolbar,它默认值为 "bottom"

src/app/photos/index.tsx
import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function PhotosScreen() { return ( <> <Stack.Toolbar> <Stack.Toolbar.Button icon={require('./assets/select.png')} onPress={() => Alert.alert('选择')} /> <Stack.Toolbar.Spacer width={24} /> <Stack.Toolbar.Button icon={require('./assets/plus.png')} onPress={() => Alert.alert('添加')} /> </Stack.Toolbar> </> ); }
src/app/photos/index.tsx
import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function PhotosScreen() { return ( <> <Stack.Toolbar> <Stack.Toolbar.Button icon="photo.on.rectangle" onPress={() => Alert.alert('选择')}> 选择 </Stack.Toolbar.Button> <Stack.Toolbar.Spacer /> <Stack.Toolbar.Button icon="plus" onPress={() => Alert.alert('添加')}> 添加 </Stack.Toolbar.Button> </Stack.Toolbar> </> ); }

信息 底部工具栏只能在页面组件中使用,不能在布局文件中使用。

间隔器

使用 Stack.Toolbar.Spacer 在工具栏项之间添加空白。其行为因平台而异:

  • AndroidStack.Toolbar.Spacer 始终需要显式的 width。目前没有可伸缩填充的间隔器。
  • iOS:不带 widthStack.Toolbar.Spacer 会在项目之间创建可伸缩空白,将它们推到两侧。这对于类似工具栏两端放按钮的布局很有用。传入 width 可实现固定大小的间距。

为按钮添加徽标(仅限 iOS)

在标题栏工具栏中,你可以添加徽标来表示数量或状态。使用 Stack.Toolbar.IconStack.Toolbar.LabelStack.Toolbar.Badge 来组合按钮内容:

src/app/inbox.tsx
import { Stack } from 'expo-router'; export default function InboxScreen() { const unreadCount = 5; return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button onPress={() => {}}> <Stack.Toolbar.Icon sf="bell" /> <Stack.Toolbar.Label>通知</Stack.Toolbar.Label> {unreadCount > 0 && <Stack.Toolbar.Badge>{String(unreadCount)}</Stack.Toolbar.Badge>} </Stack.Toolbar.Button> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

信息 徽标仅适用于 iOS 标题栏位置(leftright),不适用于底部工具栏,也不适用于 Android。

嵌入自定义视图

当你需要超越按钮和菜单的功能时,可以使用 Stack.Toolbar.View 嵌入任何 React Native 组件:

src/app/search.tsx
import { Stack } from 'expo-router'; import { Pressable, Alert } from 'react-native'; import { SymbolView } from 'expo-symbols'; export default function SearchScreen() { return ( <> <Stack.Toolbar> <Stack.Toolbar.View> <Pressable style={{ width: 32, height: 32, justifyContent: 'center', alignItems: 'center' }} onPress={() => { Alert.alert('筛选按钮已按下'); }}> <SymbolView name={{ ios: 'line.3.horizontal.decrease.circle', android: 'filter_list', }} size={24} /> </Pressable> </Stack.Toolbar.View> </Stack.Toolbar> {/* 屏幕内容 */} </> ); }

动态显示和隐藏项目

使用 hidden 属性可以根据状态切换工具栏项目的显示与隐藏:

src/app/document.tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; export default function DocumentScreen() { const [isEditing, setIsEditing] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button hidden={isEditing} icon={require('./assets/pencil.png')} onPress={() => setIsEditing(true)} /> <Stack.Toolbar.Button hidden={!isEditing} icon={require('./assets/check.png')} onPress={() => setIsEditing(false)} /> </Stack.Toolbar> {/* 文档内容 */} </> ); }
src/app/document.tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; export default function DocumentScreen() { const [isEditing, setIsEditing] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button hidden={isEditing} icon="pencil" onPress={() => setIsEditing(true)} /> <Stack.Toolbar.Button hidden={!isEditing} onPress={() => setIsEditing(false)}> 完成 </Stack.Toolbar.Button> </Stack.Toolbar> {/* 文档内容 */} </> ); }

常见问题

iOS 26 深色模式下液态玻璃工具栏按钮闪烁

采用液态玻璃样式的工具栏按钮在 iOS 26 深色模式下切换屏幕时,背景可能会出现闪烁或短暂闪白。这是因为默认主题与系统深色模式不匹配,导致液态玻璃渲染出现视觉伪影。

要修复此问题,请使用合适的主题将根布局包裹在来自 expo-router<ThemeProvider> 中:

src/app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme, Stack } from 'expo-router'; import { useColorScheme } from 'react-native'; export default function RootLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack /> </ThemeProvider> ); }
在屏幕之间导航时白色背景闪烁

屏幕切换之间出现白色闪烁通常意味着导航栈使用的是浅色背景,而你的应用使用的是深色主题。当屏幕包含工具栏项时,这种闪烁会尤其明显,因为闪烁与工具栏样式形成对比。

要修复此问题,请使用 Expo Router 的 <ThemeProvider> 包裹根布局,并传入合适的主题:

src/app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme, Stack } from 'expo-router'; import { useColorScheme } from 'react-native'; export default function RootLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack /> </ThemeProvider> ); }
滚动时大标题不会折叠

使用 headerLargeTitle: true(或 <Stack.Title large>)配合 Stack.Toolbar 时,大标题可能不会在滚动时折叠。这通常发生在可滚动视图不是屏幕组件直接第一个子元素时。

要修复此问题,请确保 ScrollViewFlatList 是由你的屏幕组件渲染的第一个子元素。如果需要包裹层,请在其上设置 collapsable={false}

src/app/index.tsx
import { Stack } from 'expo-router'; import { ScrollView, View, Text } from 'react-native'; export default function Home() { return ( <ScrollView> <Stack.Title large>Home</Stack.Title> <Text>这里是内容</Text> </ScrollView> ); }

如果你需要包裹 ScrollView,请在包裹层上设置 collapsable={false}

src/app/index.tsx
import { Stack } from 'expo-router'; import { ScrollView, View, Text } from 'react-native'; export default function Home() { return ( <View collapsable={false}> <ScrollView> <Stack.Title large>Home</Stack.Title> <Text>这里是内容</Text> </ScrollView> </View> ); }

已知限制

仅限原生

Stack.Toolbar 仅在 Android 和 iOS 上渲染。Web 没有标准工具栏,因此如果你需要在 Web 上实现工具栏行为,必须自己实现。

Android 图标必须是图像源

在 Android 上,icon 必须是 ImageSourcePropType。例如 require('./icon.png'){ uri: '...' }

你也可以使用 Stack.Toolbar.Icon 并通过 src 属性提供跨平台图标。

Android 上的间隔器需要显式宽度

可伸缩间隔器(没有 width<Stack.Toolbar.Spacer />)仅适用于 iOS。在 Android 上,不带 widthStack.Toolbar.Spacer 不会渲染任何内容——请在每个位置传入固定的 width,例如 <Stack.Toolbar.Spacer width={24} />

底部工具栏仅可用于页面组件

底部工具栏只能在页面组件中使用,不能在布局文件中使用。这是因为底部工具栏需要与特定屏幕的内容关联。

不能嵌套工具栏

你不能将 Stack.Toolbar 组件彼此嵌套。

徽标仅适用于标题位置

Stack.Toolbar.Badge 仅在使用 placement="left"placement="right" 时受支持。徽标不会显示在底部工具栏中。

Android 不支持 Badge 和 Label 原语

在 Android 上,Stack.Toolbar.Button 只会渲染图标——Stack.Toolbar.BadgeStack.Toolbar.Label 子元素会被忽略。如果你需要在 Android 上实现类似徽标的界面,可以使用 Stack.Toolbar.View 嵌入自定义组件。

Android 不支持 SearchBarSlot

Stack.Toolbar.SearchBarSlot 在 Android 上不会渲染任何内容。请使用 Stack.SearchBar 来获得跨平台搜索栏支持。

了解更多

有关完整的 API 文档,包括所有可用的属性,请参阅 Stack.Toolbar API 参考