React Navigation组件5.x-中文文档
八月_月半子关注赞赏支持React Navigation组件5.x-中文文档
引言
该文档根据React Navigation文档翻译,有些内容会根据自己的理解进行说明,不会照搬直译,若发现理解有问题的地方,欢迎大家提点!由于本人是基于iOS开发,安卓版本的目前还没有去实践运行,后续有时间会去实践,如果遇到问题,可以@我。最后,这边针对iOS运行的时候遇到的问题也有汇总,并提供解决方案。最后的最后,由于本片文章会很长,所以推荐一个Chrome插件,可以自动根据文章中的h1~h6生成目录,方便查看章节内容,在编写文章时也可以用哦! Smart TOC,点击安装后,如下图操作:
基础
1 开始
如果您已经熟悉React Native,那么您将能够快速上手React导航!如果没有学习过,你需要先读React Native Express的第1 - 4部分(包括第4部分),读完后再回到这里。
本文档的基础部分介绍React导航的最重要的方面。它足以让您了解如何构建典型的小型移动应用程序,并为您提供深入了解React导航更高级部分所需的背景知识。
1.1 安装
在RN项目中安装您需要的包
- npm
npm install @react-navigation/native
- yarn
yarn add @react-navigation/native
React导航由一些核心工具组成,并且导航器使用这些工具在应用中创建导航结构。为了提前加载安装工作,我们还需要安装和配置大多数导航器使用的依赖项,然后我们开始编写代码。
现在,我们需要安装react-native-gesture-handler
、 react-native-reanimated
、
react-native-screens
、 react-native-safe-area-context
、
@react-native-community/masked-view
这些库,如果您已经安装了这些最新版本的库,那可以跳过下面内容,否则继续阅读下去。
1.2 安装依赖到Expo管理项目
cd到你的项目目录下,运行:
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
这个命令会安装这些库的最合适的版本。接下来,您可以继续到项目中编写代码。 (注:用Expo管理项目,目前还没用到过,有疑问的童鞋麻烦自行查询!)
1.3 安装依赖到原生的RN项目中
cd到你的项目目录下,运行:
- npm
npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
- yarn
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
注意:安装后可能会出现有相关的对等依赖的警告,这个通常是由于一些不同版本的包引起的,只要您的项目可以顺利运行,可以忽略这些警告。
从RN 0.60或者更高版本开始,这些库会自动链接项目,因此不需要运行react-native link。
如果您是在mac上开发iOS项目,需要安装CocoaPods来完成项目链接:
npx pod-install ios
当您完成了react-native-gesture-handler的安装,在项目的入口文件(例如index.js或App.js)引入react-native-gesture-handler(确保在入口文件的第一行)
import 'react-native-gesture-handler';
注意:如果您忽略这一步,尽管在开发中运行是正常的,但在生产上将会奔溃。
现在,我们需要将整个app装载在NavigationContainer之中。通常做法是,在入口文件(例如index.js或App.js)做这些事情:
import 'react-native-gesture-handler';
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
);
}
注意:当您使用导航器(如堆栈导航器)时,对于任何其他依赖项,都需要遵循导航器的安装说明。如果您遇到"Unable to resolve module"的错误,您需要安装错误中提示的组件到项目中。
现在,您可以将项目编译并运行在设备或者模拟器上,并继续进行相关的代码编写。
1.4 遇到的问题
报错:TypeError: null is not an object (evaluating '_RNGestureHandlerModule.default.Direction')
解决方法:
在ios文件夹下的Podfile文件中添加:
pod 'RNGestureHandler', :path => "../node_modules/react-native-gesture-handler"
终端命令cd到ios文件,运行pod install
2 Hello React Navigation
在web浏览器上,您可能会用一个<a>标签链接到另一个页面上。当用户点击了一个链接,URL被推到浏览器历史堆栈中,当用户点击了返回按钮,浏览器会从历史堆栈中弹出之前的访问过的页面作为目前展示的页面。React Native不像web浏览器那样有内置的全局历史堆栈概念——这就是React导航的作用。
React Navigation的堆栈导航器为应用程序提供了在屏幕之间转换和管理导航历史的方法。如果您的应用程序只使用一个堆栈导航器,那么它在概念上类似于web浏览器处理导航状态——当用户与它交互时,您的应用程序从导航堆栈中推送和弹出项目,用户可以看到不同的页面。它在web浏览器和React导航中的工作方式的一个关键区别是,React导航的堆栈导航器提供了在Android和iOS中导航堆栈中的路由时需要的手势和动画。
让我们从演示最常见的导航器开始,createStackNavigator。
2.1 安装堆栈导航器库
到目前为止,我们安装是导航器的构建块和共享基础的库,React Navigation中的每个导航器都在自己的库中。要使用堆栈导航器,我们需要安装@ response -navigation/stack:
- npm
npm install @react-navigation/stack
- yarn
yarn add @react-navigation/stack
提醒:
@react-navigation/stack
依赖于我们在开始章节安装的库@react-native-community/masked-view
,如果您还未安装,麻烦回到上一章节。
2.2 创建一个堆栈导航器
createStackNavigator是一个函数,它返回一个包含两个属性的对象:屏幕和导航器。它们都是用于配置导航器的React组件。导航器应该将屏幕元素作为子元素来定义路由的配置。
NavigationContainer是一个管理导航树并包含导航状态的组件。该组件必须包装所有导航器结构。通常,我们会将这个组件呈现在应用程序的根目录下,这个根目录通常是从app .js导出的组件。这里我自定义了一个路由文件,然后在app.js中引入:
//自定义一个路由文件NavigationComponent.js
import React from 'react';
import {Text, View} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
function HomeScreen({navigation}) {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Home Screen</Text>
</View>
);
}
const Stack = createStackNavigator();
function NavigationComponent() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default NavigationComponent;
//在App.js中引入
import 'react-native-gesture-handler';
import React, {Component} from 'react';
import NavigationComponent from './Sections/常用组件/NavigationComponent';
export default class App extends Component {
render() {
return <NavigationComponent />;
}
}
如果您运行这段代码,您将看到一个带有空导航栏和包含主屏幕组件的灰色内容区域的屏幕(如上所示)。您看到的导航栏和内容区域的样式是堆栈导航器的默认配置,稍后我们将学习如何配置它们。
路由名称的对大小写不敏感——您可以使用小写的home或大写的home,这取决于您。我们喜欢把路线名称大写。
屏幕唯一需要的配置是name和component props。您可以在stack navigator reference中了解更多其他可用选项的信息。
2.3 配置导航器
所有路由配置都被指定为导航器的props。我们没有向导航器传递任何props,因此它只使用默认配置。
让我们在堆栈导航器中添加第二个屏幕,并配置主屏幕首先渲染:
function HomeScreen() {...}
function DetailsScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
</View>
);
}
const Stack = createStackNavigator();
function NavigationComponent() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
现在我们的堆栈有两个路由页面,一个主页面和一个详细页面。可以使用Screen组件指定路由。屏幕组件接受一个name prop,对应于导航的路由的名称,以及一个component prop,对应于它将渲染的组件。
这里,主路由对应于HomeScreen组件,而详细信息路由对应于DetailsScreen组件。堆栈的初始路由是主路由。尝试将其更改为Details并重新加载应用程序(如您所料,React Native的快速刷新不会更新initialRouteName的更改),注意您现在将看到Details屏幕。然后将其更改为Home并重新加载一次。
注意:组件prop只接受component,而不是渲染函数。不要传递内联函数(例如component={() => <HomeScreen />}),否则当父组件重新渲染时,你的组件将会卸载和重新加载并移除所有的state。见Passing additional props来替代。
2.4 指定options
在导航器里的每个屏幕可以指定一些options,例如一个渲染页面的标题。这些options可以传递到每个屏幕组件的options prop中:
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Overview' }}
/>
有时我们希望为导航器中的所有屏幕指定相同的options。为此,我们可以向导航器传递screenOptions prop。
2.5 传递props
有时我们可能想要传递props到屏幕上。我们可以用两种方法来实现:
1.使用React context提供程序包装导航器,以便向屏幕传递数据(推荐)。
2.使用屏幕渲染回调来代替指定一个组件的prop:
<Stack.Screen name="Home">
{props => <HomeScreen {...props} extraData={someData} />}
</Stack.Screen>
注意:默认情况下,React Navigation会对屏幕组件进行优化,以防止不必要的渲染。使用渲染回调会移除这些优化。所以如果你使用渲染回调,需要为组件使用React.memo或React.PureComponent,以避免性能问题。
2.6 下一步?
接下来的问题是:“我如何从主页面跳转到详情页面?”,这将在下一节中介绍。
2.7 总结
- React Native没有像web浏览器那样的内置导航API。React导航为你提供了这个功能,以及提供iOS和Android的手势和动画来在屏幕之间切换。
- Stack.Navigator是一个组件,它将路由配置为子组件和用于配置props,并渲染内容。
- 每个Stack.Screen组件接受一个名称prop,该名称引用路由的名称,而component prop被指定为路由渲染的组件。这是2个必需的props。
- 要指定堆栈中的初始路由是什么,请设置initialRouteName作为导航器的prop。
- 要指定特定于屏幕的options,我们可以向堆栈传递一个options prop。对于常见的options,我们可以将屏幕options传递给Stack.Navigator。
3 屏幕页面切换
在前面的章节,我们定义了一个包含主页面和详情页面的堆栈路由,但是我们没有学习如何让用户导航主页面和详细页面(虽然我们学习了在代码中如何改变初始路由,但让用户强制克隆库和改变初始路由,以达到展示另一个页面,可以想象,这是最糟糕的用户体验)。
如果是web浏览器,我们可以这样:
<a href="details.html">Go to Details</a>
另一个方法是:
<a
onClick={() => {
window.location.href = 'details.html';
}}
>
Go to Details
</a>
我们将执行与全局window.location类似的操作,我们将使用navigation prop向下传递到屏幕组件。
3.1 导航到新页面
import React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}
// ... other code from the previous section
让我们来分析一下:
- navigation - 在堆栈导航器中,navigation这个属性被传递到每个屏幕组件(定义)中。稍后将在"深入导航属性"中详细介绍 。
- navigate('Details') - 我们用路由的名称来调用导航函数来达到用户想要看到的页面(在导航属性上——命名是困难的!)。
如果我们使用navigation.navigate,导航到未在堆栈导航器上定义的路由名称,将不会发生任何事情。换句话说,我们只能导航到在堆栈导航器上定义的路由——不能导航到任意组件。 现在在我们的栈上有两个路由:(1)主路由,(2)详情路由。如果我们在详情页面再次导航到详情页面,会发生什么?
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}
如果你运行这段代码,当你点击"Go to Details... again"后,不会做任何事情!因为我们已经在详情页面上了。
假设,我们确实想跳转到一个新的详情页面。这在向每个路由传递一些唯一数据的情况下非常常见(稍后在讨论参数时将对此进行更多讨论!)。我们可以用navigate的push来达到目的,这个允许添加一个路由而不必理会栈上是否有这个路由。
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
每次使用push,在栈导航器上都会新增一个路由。而当使用navigate时,首先会判断是否有这个名称的路由,当栈上没有这个路由时才会跳转到一个新的路由页面。
3.2 返回
在路由中,当前页面的头部会有一个返回按钮,点击返回按钮可以回到之前的页面(但如果在路由中只有一个页面,头部没有返回按钮,也无法操作返回按钮)。
有时候你想通过编写代码来完成这个操作,我们使用navigation.goBack();
这样做:
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}
在Android上,React Navigation在用户点击硬件返回按钮时,返回事件是一样的。 另一个常见的需求是多个页面的返回--例如,你已经跳转了多个页面,想直接回到第一个页面,在本例中,我们要返回Home,所以我们可以使用navigate('Home')(而不是push!试试看,看看有什么不同)。另一种选择是navigation.popToTop(),它返回到堆栈中的第一个屏幕。
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
<Button
title="Go back to first screen in stack"
onPress={() => navigation.popToTop()}
/>
</View>
);
}
3.3 总结
- navigation.navigate('RouteName')如果新路由不在堆栈中,则将其推到堆栈导航器,否则将跳转到这个页面。
- navigation.push('RouteName')可以多次跳转同一个路由页面。
- header bar会自带一个返回按钮提供返回操作,但是我们可以使用navigation.goBack()来实现返回操作。在Android上的硬件返回按钮的返回操作是一样的效果。
- 你能使用navigation.navigate('RouteName')返回到一个已经存在的页面,并且你可以使用navigation.popToTop()返回第一个页面。
- 在所有的页面组件中都可以获取到navigation属性(只要组件被定义为路由配置和使用React Navigation来渲染路由)。
4 路由间传递参数
还记得我说过”稍后在讨论参数时将对此进行更多讨论!“吗?现在可以开始了。
现在我们知道如何在栈导航器上创建一些路由,并且路由之间的跳转,那么如何在路由之间传递参数呢,我们来看看。
这里有两部分:
- 将路由上需要的参数放在一个对象里,作为navigation.navigate函数的第二个参数:
navigation.navigate('RouteName', { /* params go here */ })
- 在组件中获取这个参数:
route.params
。
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => {
/* 1. Navigate to the Details route with params */
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
}}
/>
</View>
);
}
function DetailsScreen({ route, navigation }) {
/* 2. Get the param */
const { itemId } = route.params;
const { otherParam } = route.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Button
title="Go to Details... again"
onPress={() =>
navigation.push('Details', {
itemId: Math.floor(Math.random() * 100),
})
}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}
4.1 更新参数
页面上也可以更新参数,类似更新页面状态。navigation.setParams就可以用来更新页面参数。通过API reference for了解更多。
你也可以向页面传递一些初始参数。如果导航到页面并没有设置任何参数,这个初始参数将会被使用。它们会与传递的参数进行浅合并。初始参数被指定为initialParams 属性:
<Stack.Screen
name="Details"
component={DetailsScreen}
initialParams={{ itemId: 42 }}
/>
4.2 传递参数到之前的页面
不仅仅能传递参数到新的页面,也能传递参数到之前的页面。例如,在页面上创建一个post按钮,并且点击这个按钮创建一个新的post页面。在创建了post页面以后,你想传递一些数据到上一个页面。
想做到这个,你可以使用navigate的方法,如果页面存在的话,可以使用像goBack这样的方法。你可以通过navigate携带参数将参数传回去:
function HomeScreen({ navigation, route }) {
React.useEffect(() => {
if (route.params?.post) {
// Post updated, do something with `route.params.post`
// For example, send the post to the server
}
}, [route.params]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button
title="Create post"
onPress={() => navigation.navigate('CreatePost', {
text: route.params?.post ? route.params?.post : '',
})}
/>
<Text style={{ margin: 10 }}>Post: {route.params?.post}</Text>
</View>
);
}
function CreatePostScreen({ navigation, route }) {
//将已经输入的内容,传递过来赋值给TextInput的处理逻辑
let {text} = route.params;
let [postText, setPostText] = React.useState(text);
return (
<>
<TextInput
multiline
placeholder="What's on your mind?"
style={{ height: 200, padding: 10, backgroundColor: 'white' }}
value={postText}
onChangeText={setPostText}
/>
<Button
title="Done"
onPress={() => {
// Pass params back to home screen
navigation.navigate('Home', { post: postText });
}}
/>
</>
);
}
当点击”Done“按钮,TextInput输入的内容就会回传到主页面上并刷新展示在页面上。
4.3 嵌套的导航页面传递参数
如果你有一个嵌套的导航器,传递参数有些许不同。例如,你有一个叫做Account的页面,想要传递参数到Settings页面。需要做如下操作:
navigation.navigate('Account', {
screen: 'Settings',
params: { user: 'jane' },
});
了解更多请点击Nesting navigators
4.4 总结
- navigate和push在导航页面时可以作为可选的第二个参数传递参数。例如:
navigation.navigate('RouteName', {paramName: 'value'})
。 - 在页面上可以通过
route.params
读取传递的参数。 - 可以通过
navigation.setParams
更新页面参数。 - 通过
initialParams
属性可以传递初始化的参数。
5 配置header bar
我们已经知道怎么配置header标题,但是让我们在学习其他options之前再回顾一遍——复习是学习的关键
5.1 设置header标题
组件接受options prop,它是一个对象或返回一个对象的函数,它包含各种配置选项。其中一个就是title,看下面的例子:
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My home' }}
/>
</Stack.Navigator>
);
}
5.2 在title中使用参数
为了在title中使用参数,我们需要为页面设置一个返回配置对象的函数。在options中尝试使用this.props是很有用的,但是在组件渲染之前,没有引用组件的实例,因此没有可用的props。相反,如果我们将options设置为一个函数,那么React Navigation将用一个包含{Navigation, route}的对象调用它——在这种情况下,我们所关心的是route,它与作为route prop传递给页面prop的对象是同一对象。还记得我们可以通过route得到参数。参数,下面我们用这个来提取参数并使用它作为标题。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My home' }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({ title: route.params.name })}
/>
</Stack.Navigator>
);
}
传入选项函数的参数是一个具有以下属性的对象:
- navigation - 页面navigation prop
- route - 页面route prop
在上面的例子中我们只需要route属性,在某些案例中,我们还需要用到navigation属性。
5.3 用setOptions更新options
通常需要在安装的屏幕组件本身更新活动页面的选项配置。我们可以使用navigation.setOptions来做实现。
/* Inside of render() of React class */
<Button
title="Update the title"
onPress={() => navigation.setOptions({ title: 'Updated!' })}
/>
在Snack上尝试"updating navigation options"
5.4 调整header样式
自定义header样式有3个关键属性:headerStyle
,headerTintColor
,和 headerTitleStyle
。
- headerStyle:应用于包装头部的视图的样式对象。如果你设置它的backgroundColor,就是设置header的颜色。
- headerTintColor:back按钮和标题都使用这个属性作为字体颜色。在下面的示例中,我们将色调设置为白色(#fff),这样返回按钮和标题标题就是白色的。
- headerTitleStyle:可以使用它来自定义fontFamily、fontWeight和其他text样式。
function NavigationComponent() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: 'My home',
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>
</Stack.Navigator>
);
}
需要注意:
- 在iOS上,状态栏上的字体和icon是黑色的,在深色的背景上显示不是很友好,我们不会在这里讨论它,但是您应该确保配置状态栏,使其适应状态栏指南您的屏幕颜色。
- 我们仅仅在主页面配置了这些,当跳转到详情页面时,header又会恢复原来的样式。接下来,我们看看如何在页面之间共享样式。
5.5 页面之间共享options
通常,在多个页面上我们会设置一个相同样式的header。例如,你的公司品牌颜色是红色,然后页面header想设置为红色背景和白色字体。在上面的例子中,我们配置了主页面header的颜色,当我们跳转到详情页面时,header恢复到默认的样式了。如果我们将主页面的样式配置复制到详情页面上,那会很麻烦,如果app每个页面都需要这个样式呢?我们不用这么做,我们可以将样式配置移动到Stack.Navigator下的screenOptions属性上。
function StackScreen() {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My home' }}
/>
</Stack.Navigator>
);
}
在Snack上尝试"sharing header styles"
现在,属于StackScreen上的页面都会有一个主题样式,但是,如果我们需要重载这些配置,有什么方法呢?
5.6 用自定义组件代替标题
有时候,你不仅仅只是想改变title的text和样式 -- 比如,你想用一张图片替代标题,或者将标题设置为按钮。在这些案例中,你完全可以使用自定义的组件来重载标题。
function LogoTitle() {
return (
<Image
style={{ width: 50, height: 50 }}
source={require('@expo/snack-static/react-native-logo.png')}
/>
);
}
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerTitle: props => <LogoTitle {...props} /> }}
/>
</Stack.Navigator>
);
}
在Snack上尝试"custom header title component"
你可能注意到,为什么我们设置headerTitle为一个组件,而不是像之前一样设置title?因为headerTitle是Stack.Navigator的一个属性,headerTitle默认是一个Text组件,用来展示title的。
5.7 额外的配置
你可以在createStackNavigator
reference里浏览stack navigator里所有的配置列表。
5.8 总结
- 你可以通过options里的属性自定义页面的header样式。阅读所有的options in the API reference。
- options可以是一个对象或者一个函数。当是函数时,它提供了一个包含navigation和route属性的对象。
- 在初始化时,可以通过screenOptions配置一个共享的样式。这个属性优先级高于配置。
6 Header buttons
现在我们知道如何去定义header的外观了,接下来的操作,可能会让我们更有动力:通过自定义配置来响应用户的触摸。
6.1 在header上添加button
大多数header上的交互是在左边或者右边有个按钮可以点击。让我们在header右边添加一个按钮(这是整个屏幕上最难触摸的地方之一,取决于手指和手机的大小,但也是放置按钮的正常位置)。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerTitle: props => <LogoTitle {...props} />,
headerRight: () => (
<Button
onPress={() => alert('This is a button!')}
title="Info"
color="#fff"
/>
),
}}
/>
</Stack.Navigator>
);
}
当我们通过这个方法定义按钮,由于它不是HomeScreen实例的变量选项,因此不能使用setState或者其他实例方法去改变它。这是非常重要的,因为在header上添加button交互是非常常见的。因此,我们看看接下来怎么做。
6.2 Header与页面组件的交互
为了能够与页面组件交互,我们使用navigation.setOptions代替options属性来定义button。在页面组件中使用navigation.setOptions,我们可以访问页面的props,state,context等。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={({ navigation, route }) => ({
headerTitle: props => <LogoTitle {...props} />,
})}
/>
</Stack.Navigator>
);
}
function HomeScreen({ navigation }) {
const [count, setCount] = React.useState(0);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<Button title="Update count!" onPress={() => setCount((c) => c + 1)} />
),
});
}, [navigation, setCount]);
return <Text>Count: {count}</Text>;
}
6.3 自定义返回按钮
createStackNavigator提供了特定平台的默认的返回按钮,在iOS上包含button和text,在可用空间中,text展示的是上个页面的标题,否则只展示Back内容。
你可以通过headerBackTitle和headerTruncatedBackTitle来修改label的行为(更多)。
你可以使用headerBackImage自定义返回按钮的图片。
6.4 重写返回按钮
只要用户可以从当前页面返回之前的页面,返回按钮就会自动被渲染到栈导航器上--换句话说,只要在栈上有超过一个页面,返回按钮就会自动被渲染。
一般来说,这就是你想要的。但在某些情况下,您可能更希望定制back按钮,而不是通过上面提到的选项,在这种情况下,您可以将headerLeft选项设置为将要呈现的React元素,就像我们对headerRight所做的那样。另外,headerLeft选项还接受一个React组件,例如,可以使用该组件重写back按钮的onPress行为。更多相关内容请参阅api reference。
6.5 总结
- 你可以用options里的headerLeft和headerRight属性,在header上设置buttons。
- 返回按钮可以完全使用headerLeft自定义,但假如你仅仅只是想修改title和image,有其他的属性可以设置--headerBackTitle、headerTruncatedBackTitle,和headerBackImage。
7 嵌套的导航
嵌套的导航意思是在一个导航上的页面里渲染另一个导航,例如:
首先做下前期工作,安装需要的组件库,这里需要用到@react-navigation/bottom-tabs
:
- npm
npm install @react-navigation/bottom-tabs
- yarn
yarn add @react-navigation/bottom-tabs
然后开始我们的编码:
/* 自定义NestingNavigators.js文件,在App.js中引入即可 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
function Feed() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#e3e',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Feed Screen</Text>
<Button
title="Go to Profile"
onPress={() => navigation.navigate('Profile')}
/>
</View>
);
}
function Messages() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#b33',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Messages Screen</Text>
</View>
);
}
const Tab = createBottomTabNavigator();
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={Feed} />
<Tab.Screen name="Messages" component={Messages} />
</Tab.Navigator>
);
}
function Profile() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#a3e',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Profile Screen</Text>
</View>
);
}
function Settings() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#e3a',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Settings Screen</Text>
</View>
);
}
const Stack = createStackNavigator();
function NestingNavigators() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default NestingNavigators;
(注:官方文档代码省略了很多,我这边弄了个完整的,有兴趣的童鞋也可以自己实现!)
在上面的例子中,Home组件包含了一个tab导航。在整个App组件中,Home组件也被用在stack导航的Home页面上。因此,这里的tab导航是嵌套在stack导航里面的:
* Stack.Navigator
。Home (Tab.Navigator)
. Feed (Screen)
. Messages (Screen)
。Profile (Screen)
。Settings (Screen)
嵌套导航器的工作原理非常类似于嵌套常规组件。为了实现您想要的行为,通常需要嵌套多个导航器。
7.1 嵌套导航有什么影响
当嵌套导航时,有些东西要注意:
7.1.1 每个导航器保存自己的导航历史
比如,在一个嵌套的stack导航里点击了返回按钮,它将返回到嵌套的栈内的上一个页面,即使有另一个导航器作为父视图。
7.1.2 导航操作由当前导航器处理,如果无法处理就由它的父导航处理
例如,如果你在一个嵌套的页面上调用navigation.goBack(),如果已经在第一个页面上了,那么仅仅会返回到父导航。其他操作比如navigate在效果上是一样的,例如,在嵌套的导航里操作导航,但这个操作没有处理,然后父导航将会处理这个操作。在上例中,当在Feed页面上调用navigate('Messages'),这个嵌套的tab导航会处理它,但是如果你调用navigate('Settings'),父栈导航器将会处理它。
7.1.3 嵌套导航不接收父事件
例如,你有一个栈导航嵌套在tab导航里,在栈导航上的页面不会收到父tab导航发出的通知事件,比如(tabPress)时添加navigation.addListener监听。为了能收到父导航的事件,你可以使用navigation.dangerouslyGetParent().addListener来监听父事件。
7.1.4 父导航的UI渲染在子导航的顶部
例如,当嵌套一个栈导航到折叠(抽屉)导航里,你会看到折叠页面在栈导航的头部。然而,如果你嵌套一个折叠导航到栈导航里,折叠页面会出现在header下面。这是一个很重要的点,对于你决定如何嵌套导航。
在你的App中,你可能会根据你想要的行为使用这些模式:
- 栈导航嵌套在折叠导航的每个页面中-折叠页面出现在栈的header上面。
- Tab导航嵌套在栈导航的初始页面里-当push页面时,新页面会覆盖tab bar。
- 栈导航嵌套在tab导航的每个页面中-tab bar总是可见的。点击tab会再次pops到栈顶。
7.2 导航到一个嵌套导航里的页面
考虑下面的例子:
function Root() {
return (
<Stack.Navigator>
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
}
const Drawer = createDrawerNavigator();
function NestingNavigators() {
return (
<NavigationContainer>
<Drawer.Navigator>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Root" component={Root} />
</Drawer.Navigator>
</NavigationContainer>
);
}
这里,你可能想从Home页面导航到Root栈页:
navigation.navigate('Root');
这是可以的,Root组件里的初始页面会展示,没错,就是Profile。但有时,你可能想在导航上控制想展示的页面。通过下面操作,你能在参数里指定页面的名称:
navigation.navigate('Root', { screen: 'Settings' });
现在,Settings将会代替Profile展示在导航上。
这看起来可能与以前使用嵌套页面的导航方式非常不同。与之前的差异是,所有的配置都是静态的,因此,React Navigation可以通过递归到嵌套配置中,静态地找到所有导航器及其页面的列表。但是使用动态配置,React Navigation不知道哪些页面可用,在哪里可用,直到导航器包含页面渲染后。通常,还没导航到一个页面的时候,这个页面的内容不会被渲染,所以还没有渲染的导航器的配置是不可用的。这就需要指定导航到的层次结构。这也是为什么你应该使用尽可能少的导航器嵌套以使代码更简单的原因。
7.2.1 在嵌套导航传递参数到页面
你也可以通过一个指定的参数key传递参数:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
如果导航已经渲染,在使用栈导航时跳转到另一个页面就会push到一个新的页面。
对于深度嵌套的屏幕,可以采用类似的方法。注意navigate的第二个参数,也就是params,也可以这样做:
navigation.navigate('Root', {
screen: 'Settings',
params: {
screen: 'Sound',
params: {
screen: 'Media',
},
},
});
上面的案例中,你想要导航到Media页面,他是嵌套在Sound页面里面的,Sound页面又嵌套在Settings页面里。
7.2.2 在导航器里渲染被定义的初始路由
默认情况下,当导航到一个嵌套在导航里的页面时,被指定的页面会被用于初始页面,并且初始路由属性会被忽略。这个不同于React Navigation 4。
如果你想在导航里想渲染一个指定的初始页面,通过设置initial: false,您可以禁用使用指定页面作为初始页面的行为:
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
7.3 嵌套时的最佳实践
我们推荐减少嵌套导航到最小。尝试用尽可能少的嵌套来实现您想要的行为。嵌套有很多缺点:
- 当导航到嵌套的页面时,代码变得难以理解
- 它会导致深度嵌套的视图层次结构,会导致低端设备的内存和性能问题
- 嵌套相同类型的导航器(例如tab在tab里,drawer在drawer里等等)会导致令人困惑的用户体验。
将嵌套导航器看作是实现您想要的UI的一种方式,而不是组织代码的一种方式。如果您希望为组织创建单独的页面组,请将它们保存在单独的对象/数组中,而不是保存在单独的导航器中。
8 Navigation生命周期
在前面的部分,我操作了一个包含Home和Details两个页面的栈导航,并且使用navigation.navigate('RouteName')在路由间导航。
在这方面的一个重要问题是:当我们离开Home页面时或返回到Home页面时发生了什么?用户如何知道离开了或返回到一个路由页面?
如果您从web角度来看react-navigation,那么您可以假设当用户从路由a导航到路由B时,A将被卸载(componentWillUnmount会被调用),当返回到A时,又会被加载。而这些React生命周期方法仍然有效并被使用在react-navigation,它们的用法和web是有区别的。这是由更复杂的移动导航需求驱动的。
8.1 示例场景
有一个包含A和B页面的栈导航。导航到A页面后,它的componentDidMount函数被调用。当push到B页面,它的componentDidMount也被调用,但是A仍然挂载在栈上,因此不调用它的componentWillUnmount。
当从B页面回到A时,B页面的componentWillUnmount被调用,但A的componentDidMount不会被调用,因为A在整个生命周期一直在栈上。
与其他导航器(结合使用)也可以观察到类似的结果。考虑一个tab导航上有两个tab,每个tab都在栈导航上:
/* Navigation生命周期 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
/*这里就省略了,减少篇幅,具体代码可以在前面代码里找*/
function Profile() {...}
function Settings() {...}
function HomeScreen() {...}
function DetailScreen() {...}
const Tab = createBottomTabNavigator();
const SettingsStack = createStackNavigator();
const HomeStack = createStackNavigator();
function NavigationLifeCycle() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="First">
{() => (
<SettingsStack.Navigator>
<SettingsStack.Screen name="Settings" component={Settings} />
<SettingsStack.Screen name="Profile" component={Profile} />
</SettingsStack.Navigator>
)}
</Tab.Screen>
<Tab.Screen name="Second">
{() => (
<HomeStack.Navigator>
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Details" component={DetailScreen} />
</HomeStack.Navigator>
)}
</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
);
}
export default NavigationLifeCycle;
刚开始在HomeScreen,然后导航到DetailsScreen。然后切换tab展示SettingsScreen页面,再导航到ProfileScreen。这部分操作已经完成,4个页面都已经加载了!如果你用tab切换回HomeStack,你将会看到DetailsScreen页面-HomeStack已经保存了导航状态。
8.2 React Navigation 的生命周期事件
现在我们已经了解了React生命周期方法是如何在React Navigation中工作的,让我们解答一下刚开始的问题:”用户如何知道离开(blur)了或返回(focus)到一个路由页面?“
React Navigation向订阅事件的页面组件发出事件。我们可以监听focus和blur事件,就可以知道是否进入当前页面或离开了当前页面。
例如:
function Profile({navigation}) {
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
//Screen was focused
//Do sometings
console.log('Profile page is focused!!!');
});
return unsubscribe;
}, [navigation]);
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#a3e',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Profile Screen</Text>
</View>
);
}
参阅Navigation events了解更多事件和API的用法
我们可以使用useFocusEffect来执行,而不是手动添加事件监听器。它类似于React的useEffect,但它与导航生命周期紧密相连。
例如:
import { useFocusEffect } from '@react-navigation/native';
function Profile({navigation}) {
useFocusEffect(
React.useCallback(() => {
//Do something when the screen is focused
console.log('Profile page is focused!!!');
return () => {
//Do something when the screen is unfocused
// Useful for cleanup functions
console.log('Profile page is unfocused!!!');
};
}, []),
);
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
backgroundColor: '#a3e',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Profile Screen</Text>
</View>
);
}
如果你想根据页面是否被聚焦来渲染不同的东西,你可以使用useIsFocused,它返回一个指示页面是否被聚焦的布尔值。
8.3 总结
- 虽然React的生命周期方法仍然有效,但React导航添加了更多的事件,可以通过navigation属性来订阅。
- 你也可以用useFocusEffect或者useIsFocused来代替。
9 打开full-screen模态
模态显示临时的与主视图交互的内容。
模态类似于一个弹框 -- 它不是你的主要导航流程的一部分 -- 它通常有一个不同的过渡,不同的方法dismiss它,并且专注于某一特定的内容或交互。
将其作为React导航基础原理的一部分进行解释的目的不仅仅是因为这是一个常见的用例,还因为需要嵌套导航器的知识来实现,它是React Navigation重要的一部分。
9.1 创建模态栈
/* Navigation生命周期 */
import React from 'react';
import {Text, View, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
function HomeScreen({navigation}) {
return (
// eslint-disable-next-line react-native/no-inline-styles
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text style={{fontSize: 30}}>This is the home screen!</Text>
<Button
title="Open Modal"
onPress={() =>
/* 1.传递参数到详情页面 */
navigation.navigate('MyModal')
}
/>
</View>
);
}
function DetailScreen() {
return (
// eslint-disable-next-line react-native/no-inline-styles
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Detail Screen</Text>
</View>
);
}
function ModalScreen({navigation}) {
return (
// eslint-disable-next-line react-native/no-inline-styles
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text style={{fontSize: 30}}>This is a Modal!</Text>
<Button title="Dismiss" onPress={() => navigation.goBack()} />
</View>
);
}
const MainStack = createStackNavigator();
const RootStack = createStackNavigator();
function MainStackScreen() {
return (
<MainStack.Navigator>
<MainStack.Screen name="Home" component={HomeScreen} />
<MainStack.Screen name="Details" component={DetailScreen} />
</MainStack.Navigator>
);
}
function NavigationModal() {
return (
<NavigationContainer>
<RootStack.Navigator mode="modal">
<RootStack.Screen
name="Main"
component={MainStackScreen}
options={{headerShown: false}}
/>
<RootStack.Screen
name="MyModal"
component={ModalScreen}
options={{headerShown: false}}
/>
</RootStack.Navigator>
</NavigationContainer>
);
}
export default NavigationModal;
(注:官方文档里提供的代码有点问题,我这边完善了一下)
有些重要的事情要注意一下:
-
我们用MainStackScreen组件作为RootStackScreen的一个页面!这里,我们是嵌套了一个栈导航在另一个栈导航里。这样做是完全有效的,因为我们想要使用模态实现不同的过渡方式。由于RootStackScreen渲染一个栈导航器,并有自己的header,我们也想隐藏这个页面的header。在将来它将是重要的,对于tab导航,例如,每个tab会有自己的栈。直观上,这就是你所期望的:当你从tab A切换到tab B,在继续浏览tab B时,您希望tab A保持其导航状态。请看这张图,在这个例子中可以看到导航的结构:
image.png
-
在栈导航上,mode属性可以是:card(默认)和 modal。在iOS上,modal的行为从底部滑动屏幕,并允许用户从顶部向下滑动来关闭它。modal属性在Android上没有影响,因为全屏模式在平台上没有任何不同的转换行为。
-
当我们调用navigate时,我们不需要指定任何东西除了我们想要导航到的路由。不需要限定它属于哪个堆栈(任意命名的“根”或“主”堆栈)——React导航尝试在最近的导航器上查找路由,然后在那里执行操作。为了使其可视化,请再次查看上面的树形图,并想象navigate动作从主屏幕流向主堆栈。我们知道MainStack不能处理路由MyModal,所以它会将它流到RootStack,后者能处理那个路由,所以它确实做到了。
9.2 总结
-
在栈导航上想要改变过渡方式,可以使用mode属性。当设置为modal时,所有的屏幕都是动态的——从下到上而不是从右到左。这适用于整个堆栈导航器,因此为了在其他页面上使用从右到左的转换,我们添加另一个具有默认配置的导航堆栈。
-
navigation.navigate导航遍历导航器树以查找可以处理导航操作的导航器。
(未完待续)
评论3 赞1 1赞2赞 赞赏