在 React Navigation 6 中组合使用 Drawer、Tab 和 Stack 导航器
原文发布于https://blog.deversity.com/2021/10/combining-drawer-tab-and-stack.html
今天我们将使用抽屉式、标签式和堆栈式导航器。我们将介绍两种情况:
- 一个更简单的场景是,我们在单个抽屉式路由中使用选项卡导航器。
- 更复杂的流程是,我们希望标签栏在所有抽屉式导航中都可见且可访问。
在第二个示例中,我们将尝试克服 React Navigation 的一个设计限制——不同的导航器如果一起使用,只能相互嵌套,因此不能相互交织。
介绍
使用 React Navigation 库可以极大地简化 React Native 应用的导航功能。它提供了多种类型的导航器,并具备强大的自定义能力。在一些简单的场景下,我们只需使用一种导航器即可,但通常情况下,我们需要在一个应用中组合使用多种类型的导航器。
我们选择的例子是为一家连锁酒店开发一款应用程序。部分功能包括预订酒店房间、浏览不同酒店位置以及使用奖励积分。以下是我们即将开发的应用程序预览:

我们可以立即看到抽屉式导航器和标签式导航器的使用。我们还将把每个路由都实现为堆栈式导航器,因为我们知道,例如,图书流程将包含多个屏幕。
入门
(如果您是第一次参与 React Native 项目,请在继续操作之前阅读官方入门指南)
让我们初始化一个新项目。在终端中,导航到一个空目录并运行以下命令:
$ npx react-native init NavigationDemo --version 0.64.2
撰写本文时,已安装的 React 版本为 17.0.2,而 React Native 版本为 0.64.2。
接下来,我们来安装 React Navigation 及其依赖项:
$ npm install @react-navigation/native react-native-screens react-native-safe-area-context react-native-gesture-handler react-native-reanimated @react-navigation/stack @react-navigation/drawer @react-navigation/bottom-tabs
如果你正在开发 iOS 应用,还需要安装 pods:
$ cd ios; npx pod install; cd ..
请将文件中的内容替换App.js为以下代码:
import React from 'react'
import { SafeAreaView, View, StatusBar, StyleSheet, Text } from 'react-native'
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<View>
<Text>Hello navigation!</Text>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
overflow: 'hidden',
},
})
export default App
堆叠和抽屉导航器
现在我们可以开始为应用添加不同的导航器了。记住,在这个示例中,我们希望 DrawerNavigator 成为应用中的主要(始终可见)导航器,而 BottomTabNavigator 则在抽屉中聚焦到“首页”路由时显示。首先,让我们在项目中添加以下文件结构(目前所有文件均为空):
您可以从本教程末尾提供的 GitHub 代码库下载hotel_logo,也可以使用您自己的代码库。接下来,我们将创建包含三个路由(即我们的堆栈导航器)的抽屉导航器。目前,每个堆栈导航器只包含一个直接在堆栈文件中定义的屏幕。在实际应用中,堆栈导航器可以包含多个屏幕,但至少需要一个屏幕。以下是堆栈导航器文件的内容:
HomeStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Home = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home screen!</Text>
</View>
)
const HomeStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Home" component={Home} />
</Stack.Navigator>
)
}
export default HomeStackNavigator
MyRewardsStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const MyRewards = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>MyRewards screen!</Text>
</View>
)
const MyRewardsStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="MyRewards" component={MyRewards} />
</Stack.Navigator>
)
}
export default MyRewardsStackNavigator
LocationsStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Locations = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Locations screen!</Text>
</View>
)
const LocationsStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Locations" component={Locations} />
</Stack.Navigator>
)
}
export default LocationsStackNavigator
我们稍后会解释 screenOptions。现在我们已经定义了抽屉堆栈导航器,可以创建 DrawerNavigator 了:
DrawerNavigator.js:
import * as React from 'react'
import { createDrawerNavigator } from '@react-navigation/drawer'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import MyRewardsStackNavigator from './stack-navigators/MyRewardsStackNavigator'
import LocationsStackNavigator from './stack-navigators/LocationsStackNavigator'
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name="HomeStack" component={HomeStackNavigator} />
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} />
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} />
</Drawer.Navigator>
)
}
export default DrawerNavigator
并将其添加到我们的导航容器中。App.js
...
import { NavigationContainer } from '@react-navigation/native'
import DrawerNavigator from './src/navigation/DrawerNavigator'
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
</SafeAreaView>
)
}
...
让我们运行代码,看看目前为止的结果。运行
$ npx react-native start
启动 Metro 打包器。然后,在另一个终端中运行
$ npx react-native run-android
或者
$ npx react-native run-ios
根据您正在开发的平台而定(如果您想同时在两个平台上工作,请依次运行这两个命令)。
现在我们可以看到结果。我们有 React Navigation 的默认头部,一个用于打开抽屉的图标,以及抽屉菜单中的堆栈。我们可以自由地在这些堆栈之间切换。
现在让我们回到screenOptions我们在堆栈导航器中定义的部分。尝试设置headerShown: true它HomeStackNavigator并观察会发生什么:
Home 组件的头部信息渲染在 Drawer Navigator 的下方。这是因为父级导航器的 UI 渲染在子级导航器的上方。显然,我们只需要一个头部信息,因此headerShown: false为每个堆栈导航器指定screenOptions隐藏默认的堆栈头部信息即可。请注意,Drawer 头部信息中显示的标题是 `<header>` HomeStack,而不是 ` Home<header>`。如果我们导航到 HomeStack 中的另一个屏幕,标题不会改变。我们是否可以保留堆栈头部信息并隐藏 Drawer 头部信息?可以!但目前,我们希望保留默认的 Drawer 头部信息,因为它提供了一种便捷的方式来打开抽屉——只需点击头部信息中的菜单图标即可。
标签导航器
我们已经为应用添加了抽屉式导航,并定义了带有屏幕的堆栈导航器,以便添加到抽屉菜单中。现在我们需要为首页路由添加标签导航。首先,让我们以与之前相同的方式定义“预订”和“联系”堆栈导航器:
BookStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Book = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Book screen!</Text>
</View>
)
const BookStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Book" component={Book} />
</Stack.Navigator>
)
}
export default BookStackNavigator
ContactStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Contact = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Contact screen!</Text>
</View>
)
const ContactStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Contact" component={Contact} />
</Stack.Navigator>
)
}
export default ContactStackNavigator
现在让我们创建标签导航器。
BottomTabNavigator
import * as React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import BookStackNavigator from './stack-navigators/BookStackNavigator'
import ContactStackNavigator from './stack-navigators/ContactStackNavigator'
const Tab = createBottomTabNavigator()
const BottomTabNavigator = () => {
return (
<Tab.Navigator screenOptions={{
headerShown: false,
}}>
<Tab.Screen name="HomeStack" component={HomeStackNavigator} />
<Tab.Screen name="BookStack" component={BookStackNavigator} />
<Tab.Screen name="ContactStack" component={ContactStackNavigator} />
</Tab.Navigator>
)
}
export default BottomTabNavigator
请注意,我们添加的第一个标签页是 HomeStack,它已经添加到 DrawerNavigator 中。实际上,您可以将 BottomTabNavigator 视为一个堆栈容器,初始堆栈就是 HomeStack。由于 HomeStack 中包含 Home 页面,因此标签页导航器中渲染的初始页面就是 Home 页面。因为我们希望在用户位于抽屉导航的 Home 路由时显示此页面,所以我们只需将 DrawerNavigator 中的 HomeStackNavigator 组件替换为 BottomTabNavigator 即可:
DrawerNavigator.js:
...
import BottomTabNavigator from './BottomTabNavigator'
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name="HomeTabs" component={BottomTabNavigator} />
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} />
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} />
</Drawer.Navigator>
)
}
...
让我们来看看结果:
当我们在 DrawerNavigator 的第一个路径中时,可以看到底部的标签页并进行导航。如果切换到 Drawer 中的另一个路径,这些标签页就会消失(因为标签页导航器只是 Drawer 的一个屏幕)。我们再次使用了这种方法headerShown: false来避免渲染重复的头部信息。
标题和标签设计
我们已经实现了所有技术栈,现在需要实现一些通用需求。首先,让我们为标签页添加图标。本项目将使用react-native-vector-iconsFontAwesome 图标包。完整的安装指南请点击此处查看。安装完成后,我们可以BottomTabNavigator.js按如下方式编辑文件:
import * as React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { Text, StyleSheet } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import BookStackNavigator from './stack-navigators/BookStackNavigator'
import ContactStackNavigator from './stack-navigators/ContactStackNavigator'
const Tab = createBottomTabNavigator()
const BottomTabNavigator = () => {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="HomeStack" component={HomeStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="home" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Home</Text>
}}
/>
<Tab.Screen name="BookStack" component={BookStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="bed" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Book Room</Text>
}}
/>
<Tab.Screen name="ContactStack" component={ContactStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="phone" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Contact Us</Text>
}}
/>
</Tab.Navigator>
)
}
const styles = StyleSheet.create({
tabBarLabel: {
color: '#292929',
fontSize: 12,
},
})
export default BottomTabNavigator
我们为每个堆栈指定了一个图标和一个标签。` tabBarIcon<stack>` 接收一个focused属性,我们可以用它来高亮显示当前路由(` <route>` 也可以接收此属性)。`<stack>`和 ` <route>` 属性tabBarLabel有很多可能性,其中一些在https://reactnavigation.org/docs/screen-options/中有详细介绍。 让我们在抽屉导航器中使用 `<stack>` 来更改抽屉菜单中的标题和路由名称:optionsscreenOptionsscreenOptions
DrawerNavigator.js:
import * as React from 'react'
import { View, StyleSheet, Image, Text, TouchableOpacity } from 'react-native'
import { createDrawerNavigator, DrawerContentScrollView, DrawerItem } from '@react-navigation/drawer'
import Icon from 'react-native-vector-icons/FontAwesome'
import MyRewardsStackNavigator from './stack-navigators/MyRewardsStackNavigator'
import LocationsStackNavigator from './stack-navigators/LocationsStackNavigator'
import BottomTabNavigator from './BottomTabNavigator'
const Drawer = createDrawerNavigator()
const CustomDrawerContent = (props) => {
return (
<DrawerContentScrollView {...props}>
{
Object.entries(props.descriptors).map(([key, descriptor], index) => {
const focused = index === props.state.index
return (
<DrawerItem
key={key}
label={() => (
<Text style={focused ? styles.drawerLabelFocused : styles.drawerLabel}>
{descriptor.options.title}
</Text>
)}
onPress={() => descriptor.navigation.navigate(descriptor.route.name)}
style={[styles.drawerItem, focused ? styles.drawerItemFocused : null]}
/>
)
})
}
</DrawerContentScrollView>
)
}
const DrawerNavigator = () => {
return (
<Drawer.Navigator
screenOptions={({ navigation }) => ({
headerStyle: {
backgroundColor: '#551E18',
height: 50,
},
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.toggleDrawer()} style={styles.headerLeft}>
<Icon name="bars" size={20} color="#fff" />
</TouchableOpacity>
),
})}
drawerContent={(props) => <CustomDrawerContent {...props} />}
>
<Drawer.Screen name="HomeTabs" component={BottomTabNavigator} options={{
title: 'Home',
headerTitle: () => <Image source={require('../assets/hotel_logo.jpg')} />,
headerRight: () => (
<View style={styles.headerRight}>
<Icon name="bell" size={20} color="#fff" />
</View>
),
}}/>
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} options={{
title: 'My Rewards',
headerTitle: () => <Text style={styles.headerTitle}>My Rewards</Text>,
}}/>
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} options={{
title: 'Locations',
headerTitle: () => <Text style={styles.headerTitle}>Our Locations</Text>,
}}/>
</Drawer.Navigator>
)
}
const styles = StyleSheet.create({
headerLeft: {
marginLeft: 15,
},
headerTitle: {
color: 'white',
fontSize: 18,
fontWeight: '500',
},
headerRight: {
marginRight: 15,
},
// drawer content
drawerLabel: {
fontSize: 14,
},
drawerLabelFocused: {
fontSize: 14,
color: '#551E18',
fontWeight: '500',
},
drawerItem: {
height: 50,
justifyContent: 'center'
},
drawerItemFocused: {
backgroundColor: '#ba9490',
},
})
export default DrawerNavigator

让我们来详细了解一下所有改动。首先,在抽屉式导航器中,我们可以单独更改每个抽屉项目的标题。例如,当用户位于标签导航器中时,您可能不想显示标题,而是想显示公司徽标。该headerTitle属性接受字符串和函数两种类型,为我们提供了丰富的自定义选项。此外,标题中显示的标题可以与抽屉菜单中显示的标题不同。
接下来,我们希望更改页眉的外观,使其更好地符合客户的品牌形象。我们可以通过向 DrawerNavigator 传递一个函数screenOptions并指定页眉样式和组件来实现这一点。DrawerNavigatorScreenOptions也接收该route属性。我们传递的函数会headerLeft渲染菜单图标并切换抽屉——此切换函数在 DrawerNavigatornavigation对象中可用。
最后,我们来自定义抽屉菜单。目前我们只想更改路由项的样式,但遗憾的是,没有简单的 DrawerNavigation 属性可以实现这一点。因此,我们需要传递一个自定义的 drawerContent 函数,以便为每个路由项渲染一个完全自定义的组件。我们使用传递的属性来遍历这些路由项,但我们也可以使用 `<route>` 渲染更多路由<DrawerItem>,或者在 `<route>` 顶部添加图像组件<DrawerContentScrollView>,或者使用其他任何选项。
结论
在本教程中,我们结合了抽屉式导航器、标签式导航器和堆栈式导航器,创建了一个简单的导航流程。然后,我们通过screenOptions自定义设置实现了所需的外观和风格。下一节,我们将探讨如何让抽屉式导航器和标签式导航器始终可见并保持连接。
本教程的第二部分请点击此处查看。
完整的项目可以在GitHub上找到。
文章来源:https://dev.to/deversity/combining-drawer-tab-and-stack-navigators-in-react-navigation-6-l4m




