React Native 進階(四)--導航器
導航器對比
如果你剛開始接觸,那么直接選擇Navigator就好。如果你只針對iOS平臺開發,并且想和系統原生外觀一致,那么可以選擇NavigatorIOS。如果你想更好地管理導航棧,那么應該嘗試一下NavigationExperimental。
Navigator
Navigator使用純JavaScript實現了一個導航棧,因此可以跨平臺工作,同時也便于定制。
Navigator可以在renderScene方法中根據當前路由渲染不同的組件。默認情況下新的場景會從屏幕右側滑進來,但你也可以通過configureScene方法來管理這一行為。你還可以通過navigationBar屬性來配置一個跨場景的導航欄。但我們不推薦使用跨場景的navigationBar,它的代碼邏輯維護起來很困難!建議自己在場景中用View實現自定義的導航欄。
方法
如果你得到了一個navigator對象的引用,則可以調用許多方法來進行導航:
- getCurrentRoutes() - 獲取當前棧里的路由,也就是push進來,沒有pop掉的那些。
- jumpBack() - 跳回之前的路由,當然前提是保留現在的,還可以再跳回來,會給你保留原樣。
- jumpForward() - 上一個方法不是調到之前的路由了么,用這個跳回來就好了。
- jumpTo(route) - 跳轉到已有的場景并且不卸載。
- push(route) - 跳轉到新的場景,并且將場景入棧,你可以稍后跳轉過去
- pop() - 跳轉回去并且卸載掉當前場景
- replace(route) - 用一個新的路由替換掉當前場景
- replaceAtIndex(route, index) - 替換掉指定序列的路由場景
- replacePrevious(route) - 替換掉之前的場景
- resetTo(route) - 跳轉到新的場景,并且重置整個路由棧
- immediatelyResetRouteStack(routeStack) - 用新的路由數組來重置路由棧
- popToRoute(route) - pop到路由指定的場景,在整個路由棧中,處于指定場景之后的場景將會被卸載。
- popToTop() - pop到棧中的第一個場景,卸載掉所有的其他場景。
這些都是navigator可以用的public method,就是跳轉用的,里面有些帶參數的XXX(route),這個route參數是什么呢,這個route就是:
renderScene={(route, navigator) =>
這里的route,最基本的route就是:
let route = {
component: SampleComponent
}
屬性
configureScene function
可選的函數,用來配置場景動畫和手勢。會帶有兩個參數調用,一個是當前的路由,一個是當前的路由棧。然后它應當返回一個場景配置對象
(route, routeStack) => Navigator.SceneConfigs.FloatFromRight
- Navigator.SceneConfigs.PushFromRight (默認)
- Navigator.SceneConfigs.FloatFromRight
- Navigator.SceneConfigs.FloatFromLeft
- Navigator.SceneConfigs.FloatFromBottom
- Navigator.SceneConfigs.FloatFromBottomAndroid
- Navigator.SceneConfigs.FadeAndroid
- Navigator.SceneConfigs.HorizontalSwipeJump
- Navigator.SceneConfigs.HorizontalSwipeJumpFromRight
- Navigator.SceneConfigs.VerticalUpSwipeJump
- Navigator.SceneConfigs.VerticalDownSwipeJump
initialRoute object
定義啟動時加載的路由。路由是導航欄用來識別渲染場景的一個對象。initialRoute必須是initialRouteStack中的一個路由。initialRoute默認為initialRouteStack中最后一項。
initialRouteStack [object]
提供一個路由集合用來初始化。如果沒有設置初始路由的話則必須設置該屬性。如果沒有提供該屬性,它將被默認設置成一個只含有initialRoute的數組。
navigationBar node
可選參數,提供一個在場景切換的時候保持的導航欄。
navigator object
可選參數,提供從父導航器獲得的導航器對象。
onDidFocus function
每當導航切換完成或初始化之后,調用此回調,參數為新場景的路由。
onWillFocus function
會在導航切換之前調用,參數為目標路由。
renderScene function
必要參數。用來渲染指定路由的場景。調用的參數是路由和導航器。
(route, navigator) =><MySceneComponent title={route.title} navigator={navigator} />
sceneStyle View#style
將會應用在每個場景的容器上的樣式。
例子
Navigator.js
import React from 'react';
import {
View,
Navigator
} from 'react-native';
import FirstPageComponent from './FirstPageComponent';
export default class Sample extends React.Component {
render() {
let defaultName = 'FirstPageComponent';
let defaultComponent = FirstPageComponent;
return (
<Navigator
initialRoute={{ name: defaultName, component: defaultComponent }}
configureScene={(route) => { // 跳轉動畫
return Navigator.SceneConfigs.VerticalDownSwipeJump;
}}
renderScene={(route, navigator) => {
let Component = route.component;
// 這里有個 { ...route.params },這個語法是把 routes.params 里的每個key 作為props的一個屬性
return <Component {...route.params} navigator={navigator} />
}}
/>
);
}
}
第10行: 一個初始首頁的component名字,比如我寫了一個component叫HomeComponent,
那么這個name就是這個組件的名字[HomeComponent]了。
第11行: 這個組件的Class,用來一會兒實例化成 <Component />標簽
第14行: initialRoute={{ name: defaultName, component: defaultComponent }} 這個指定了默認的頁面,
也就是啟動app之后會看到界面的第一屏。 需要填寫兩個參數: name 跟 component。(注意這里填什么參數(參數名)純粹是自定義的,
因為這個參數也是你自己發自己收,自己在renderScene方法中處理。這個示例用了兩個參數,但其實真正使用的參數只有component)
第15,16,17行: configureScene={() => {return Navigator.SceneConfigs.VerticalDownSwipeJump;}} 這個是頁面之間跳轉時候的動畫,
具體有哪些動畫可以看node_modules/react-native/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js下的源代碼。
最后的幾行: renderScene={(route, navigator) => {let Component = route.component;return <Component {...route.params} navigator={navigator} />}},
這里是每個人最疑惑的,我們先看到回調里的兩個參數:route, navigator。通過打印我們發現route里其實就是我們傳遞的name,component這兩個貨,
navigator是一個Navigator的對象,為什么呢,因為它有push pop jump...等方法,這是我們等下用來跳轉頁面用的那個navigator對象。
return <Component {...route.params} navigator={navigator} />
這里有一個判斷,也就是如果傳遞進來的component存在,那我們就是返回一個這個component,結合前面 initialRoute 的參數,我們就知道,
這是一個會被render出來給用戶看到的component,然后navigator作為props傳遞給了這個component。
所以下一步,在這個FirstPageComponent里面,我們可以直接拿到這個 props.navigator:
FirstPageComponent.js
import React from 'react';
import {
View,
Navigator,
TouchableOpacity,
Text
} from 'react-native';
import SecondPageComponent from './SecondPageComponent';
export default class FirstPageComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
id: 2,
user: null,
};
}
_pressButton() {
let _this = this;
const { navigator } = this.props;
//為什么這里可以取得 props.navigator?請注意Navigator.js中:
//<Component {...route.params} navigator={navigator} />
//這里傳遞了navigator作為props
if (navigator) {
// 入棧~ 把SecondPageComponent頁面push進棧,接著跳轉到SecondPageComponent
navigator.push({
name: 'SecondPageComponent',
component: SecondPageComponent,
//這個 params 其實來自于Navigator 里的一個方法的參數
params: { //routes.params
id: this.state.id,
//從SecondPageComponent獲取user
getUser: function (user) {
_this.setState({
user: user
})
}
}
})
}
}
render() {
if (this.state.user) {
return (
<View>
<Text>用戶信息: {JSON.stringify(this.state.user)}</Text>
</View>
);
} else {
return (
<View>
<TouchableOpacity onPress={this._pressButton.bind(this)}>
<Text>查詢ID為{this.state.id}的用戶信息</Text>
</TouchableOpacity>
</View>
);
}
}
}
這個里面創建了一個可以點擊的區域,點擊可以跳到SecondPageComponent這個頁面,實現頁面的跳轉。
現在來創建SecondPageComponent,并且讓它可以再跳回FirstPageComponent:
SecondPageComponent.js
import React from 'react';
import {
View,
Navigator,
Text,
TouchableOpacity
} from 'react-native';
import FirstPageComponent from './FirstPageComponent';
const USER_MODELS = {
1: { name: '小李', age: 18 },
2: { name: '小明', age: 20 }
};
export default class SecondPageComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
id: null
};
}
componentDidMount() {
//這里獲取從FirstPageComponent傳遞過來的參數: id
this.setState({
id: this.props.id
});
}
_pressButton() {
const { navigator } = this.props;
if (this.props.getUser) {
let user = USER_MODELS[this.props.id];
this.props.getUser(user);
}
//出棧~ 把當前的頁面pop掉,這里就返回到了上一個頁面:FirstPageComponent了
if (navigator) {
navigator.pop();
}
}
render() {
return (
<View>
<Text>獲得的參數(id): id={this.state.id}</Text>
<TouchableOpacity onPress={this._pressButton.bind(this)}>
<Text>點我跳回去</Text>
</TouchableOpacity>
</View>
);
}
}
傳遞參數和獲取參數
傳遞參數到新頁面
傳遞參數到新頁面,可以通過push。比如在一個 press的事件里:
FirstPageComponent.js
...
constructor(props) {
super(props);
this.state = {
id: 2
};
}
_pressButton() {
...
navigator.push({
name: 'SecondPageComponent',
component: SecondPageComponent,
//這個 params 其實來自于Navigator 里的一個方法的參數
params: {
id: this.state.id
}
})
...
}
params的來歷:
//Navigator.js
...
<Navigator
initialRoute={{ name: defaultName, component: defaultComponent }}
configureScene={() => {
return Navigator.SceneConfigs.VerticalDownSwipeJump;
}}
renderScene={(route, navigator) => {
let Component = route.component;
if(route.component) {
//這里有個 { ...route.params }
return <Component {...route.params} navigator={navigator} />
}
}}
/>
...
{ ...route.params }語法是把 routes.params 里的每個key 作為props的一個屬性:
//FirstPageComponent.js
...
navigator.push({
name: 'SecondPageComponent',
component: SecondPageComponent,
params: { //routes.params
id: this.state.id
}
});
...
這里的 params.id 就變成了 <Component id={routes.params.id} navigator={navigator}> 里的id屬性(props)傳遞給了下一個頁面。
//SecondPageComponent.js
...
componentDidMount() {
//這里獲取從FirstPageComponent傳遞過來的參數: id
this.setState({
id: this.props.id
});
}
...
render() {
return (
<View>
<Text>獲得的參數: id={ this.state.id }</Text>
<TouchableOpacity onPress={this._pressButton.bind(this)}>
<Text>點我跳回去</Text>
</TouchableOpacity>
</View>
);
}
返回參數到之前頁面
返回的時候,也需要傳遞參數回上一個頁面。但是navigator.pop()并沒有提供參數,因為pop()只是從 [路由1,路由2,路由3。。。]里把最后一個路由踢出去的操作,并不支持傳遞參數給倒數第二個路由,這里要用到一個概念,把上一個頁面的實例或者回調方法,作為參數傳遞到當前頁面來,在當前頁面操作上一個頁面的state:
比如FirstPageComponent傳遞id到SecondPageComponent,然后SecondPageComponent返回user信息給FirstPageComponent:
//FirstPageComponent.js
...
constructor(props) {
super(props);
this.state = {
id: 2,
user: null,
}
}
...
_pressButton() {
let _this = this;
...
params: { //routes.params
id: this.state.id,
//從SecondPageComponent獲取user
getUser: function (user) {
_this.setState({
user: user
})
}
}
...
render() {
if (this.state.user) {
return (
<View>
<Text>用戶信息: {JSON.stringify(this.state.user)}</Text>
</View>
);
} else {
return (
<View>
<TouchableOpacity onPress={this._pressButton.bind(this)}>
<Text>查詢ID為{this.state.id}的用戶信息</Text>
</TouchableOpacity>
</View>
);
}
}
//SecondPageComponent.js
...
const USER_MODELS = {
1: { name: '小李', age: 18 },
2: { name: '小明', age: 20 }
};
export default class SecondPageComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
id: null
};
}
componentDidMount() {
//這里獲取從FirstPageComponent傳遞過來的參數: id
this.setState({
id: this.props.id
});
}
_pressButton() {
const { navigator } = this.props;
if (this.props.getUser) {
let user = USER_MODELS[this.props.id];
this.props.getUser(user);
}
//出棧~ 把當前的頁面pop掉,這里就返回到了上一個頁面:FirstPageComponent了
if (navigator) {
navigator.pop();
}
}
}
...
NavigatorIOS
如果你只針對iOS平臺開發,那么可以考慮使用NavigatorIOS。它是基于 UINavigationController封裝的。
<NavigatorIOS
initialRoute={{
component: MyScene,
title: 'My Initial Scene',
passProps: { myProp: 'foo' },
}}
/>
用法類似Navigator,NavigatorIOS也使用路由對象來描述場景,但有一些重要區別。其中要渲染的組件在路由對象的component字段中指定,要給目標組件傳遞的參數則寫在passProps中。被渲染的component都會自動接受到一個名為navigator的屬性,你可以直接調用此對象(this.props.navigator)的push和pop方法。
由于NavigatorIOS使用的是原生的UIKit導航,所以它會自動渲染一個帶有返回按鈕和標題的導航欄。
你還可以看看react-native-navigation,這是一個第三方的組件,旨在于提供原生的跨平臺的導航組件。
NavigationExperimental
Navigator和NavigatorIOS都是有狀態的組件。如果你在app中多處使用這些組件,那么維護工作就會變得非常麻煩。NavigationExperimental以不同的方式實現了導航,它可以使用任何視圖來作為導航視圖,同時還用到了規約函數(reducer)自頂向下地管理狀態。正如名字中的Experimental(實驗)所示,這一組件的整體實現具有一定的實驗性,但仍然建議你嘗試一下用它去更好地管理應用的導航。
<NavigationCardStack
onNavigateBack={onPopRouteFunc}
navigationState={myNavigationState}
renderScene={renderSceneFun}
/>
引入NavigationExperimental的步驟和React Native中的其他組件一樣。在引入此組件之后,還可以進一步解構其中一些有用的子組件,比如這里我們會從中解構NavigationCardStack和 NavigationStateUtils這兩個子組件。
import React, { Component } from 'react';
import { NavigationExperimental } from 'react-native';
const {
CardStack: NavigationCardStack,
StateUtils: NavigationStateUtils,
} = NavigationExperimental;
NavigationExperimental的實現機制與Navigator和NavigatorIOS有所不同,用它來構建導航棧還需要一些額外的步驟。
第一步:定義初始狀態和根容器
首先創建一個新組件,我們會把它作為根容器,并在這里定義初始狀態。導航棧會定義在navigationState字段中,其中也包含了初始的路由定義:
import React, { Component } from 'react';
import { NavigationExperimental } from 'react-native';
const {
CardStack: NavigationCardStack,
StateUtils: NavigationStateUtils,
} = NavigationExperimental;
class Sample extends Component {
constructor(props, context) {
super(props, context);
this.state = {
// 定義初始的導航狀態
navigationState: {
index: 0, // 現在是第一頁(索引從0開始)
routes: [{key: '最初的場景'}], // 初始僅設定一個路由
},
};
// 稍后再補充此函數的實現細節
this._onNavigationChange = this._onNavigationChange.bind(this);
}
_onNavigationChange(type) {
// 稍后再補充此函數的實現細節
}
_exit() {
//exit()實現
}
render() {
return (
<Text>這是一段占位的文字。稍后會在這里渲染導航。</Text>
);
}
}
現在我們定義了一個有狀態的組件,暫時是無用的。我們的初始狀態包含了一個路由對象,以及當前頁面的索引值。但是這看起來跟Navigator的初始路由定義好像沒什么區別!回憶一下navigator對象提供了push和pop操作,看起來也非常直觀。但是前面我們說過了,現在我們會在根容器上使用規約函數來管理狀態,下面繼續。
第二步:規約導航狀態
NavigationExperimental內置了一些有用的規約函數(reducer),都放在NavigationStateUtils中。我們現在要用的兩個就是push和pop了。它們接受一個navigationState對象參數,然后返回新的navigationState對象。
據此我們可以這樣來編寫_onNavigationChange函數,在其中判斷"push"和"pop"的行為,并分別規約對應的狀態。
_onNavigationChange(type) {
// 從state中解構出navigationState
let {navigationState} = this.state;
switch (type) {
case 'push':
// push一個新路由,在這里就是一個帶有key屬性的對象。
// key必須要確保唯一性
const route = {key: 'Route-' + Date.now()};
// 調用NavigationStateUtils提供的push規約函數
navigationState = NavigationStateUtils.push(navigationState, route);
break;
case 'pop':
// 使用pop函數來彈出當前路由
navigationState = NavigationStateUtils.pop(navigationState);
break;
}
// 如果沒有實際變化,則NavigationStateUtils會返回同樣的`navigationState`
// 我們只會更新確實發生變化的狀態
if (this.state.navigationState !== navigationState) {
// 請記住更新狀態必須通過setState()方法!
this.setState({navigationState});
// 簡單講解一下上面那一句ES6語法
// 如果key和value的字面一樣,那么可以簡寫成一個,等同于下面的寫法:
// this.setState({navigationState: navigationState});
}
}
到這里,我們已經觸碰到了NavigationExperimental的精髓所在。這里我們只處理了兩種行為,實際開發中行為可能更復雜,比如可能會考慮后退(back)行為,又或者是tab間的切換過渡行為等等。
我們現在還沒寫初始場景和實際的導航器,不過別急,我們一步一步來。
第三步:定義場景
為方便起見我們先定義一個Row(行)組件。其中顯示了一些文字,并帶有點擊事件。
class TappableRow extends Component {
render() {
return (
<TouchableHighlight
style={styles.row}
underlayColor="#D0D0D0"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>
{this.props.text}
</Text>
</TouchableHighlight>
);
}
}
現在來定義實際的場景。其中用到了一個ScrollView來顯示一個垂直列表,第一行顯示當前路由對象的key字段值,后兩行用來點擊后調用導航器的push和pop方法。
class MyVeryComplexScene extends Component {
render() {
return (
<ScrollView style={styles.scrollView}>
<Text style={styles.row}>
路由: {this.props.route.key}
</Text>
<TappableRow
text="加載下一個場景"
onPress={this.props.onPushRoute}/>
<TappableRow
text="返回上一個場景"
onPress={this.props.onPopRoute}/>
</ScrollView>
);
}
}
第四步:創建導航棧
我們之前已經定義了狀態和管理狀態的規約函數,現在可以創建導航器組件了。在寫導航器的同時,我們可以使用當前路由的屬性來配置場景并渲染它了。
class MyVerySampleNavigator extends Component {
// 在這里綁定一些導航用的方法
constructor(props, context) {
super(props, context);
this._onPushRoute = this.props.onNavigationChange.bind(null, 'push');
this._onPopRoute = this.props.onNavigationChange.bind(null, 'pop');
this._renderScene = this._renderScene.bind(this);
}
// 現在我們終于可以使用“NavigationCardStack”來渲染場景。
render() {
return (
<NavigationCardStack
onNavigateBack={this._onPopRoute}
navigationState={this.props.navigationState}
renderScene={this._renderScene}
style={styles.navigator}
/>
);
}
// 根據路由來渲染場景
// `sceneProps`的具體結構定義在`NavigationTypeDefinition`的`NavigationSceneRendererProps`中
// 這里你可以根據路由的不同來返回不同的場景組件,我們這里為了簡要說明,始終只返回這一個場景組件
_renderScene(sceneProps) {
return (
<MyVeryComplexScene
route={sceneProps.scene.route}
onPushRoute={this._onPushRoute}
onPopRoute={this._onPopRoute}
onExit={this.props.onExit}
/>
);
}
}
最后把我們新做的導航器放到根容器中:
class Sample extends Component {
// 這里省略了constructor和其他的方法
render() {
return (
<MyVerySampleNavigator
navigationState={this.state.navigationState}
onNavigationChange={this._onNavigationChange}
onExit={this._exit}
/>
);
}
}
別忘了引入組件和樣式
import { NavigationExperimental, PixelRatio, ScrollView, StyleSheet, Text, TouchableHighlight } from 'react-native';
const styles = StyleSheet.create({
navigator: {
flex: 1,
},
scrollView: {
marginTop: 64
},
row: {
padding: 15,
backgroundColor: 'white',
borderBottomWidth: 1 / PixelRatio.get(),
borderBottomColor: '#CDCDCD',
},
rowText: {
fontSize: 17,
},
buttonText: {
fontSize: 17,
fontWeight: '500',
},
});
React Native學習筆記--進階(一)--嵌入到Android原生應用中、組件的生命周期、顏色、圖片、觸摸事件
React Native學習筆記--進階(二)--動畫
React Native學習筆記--進階(三)--定時器、直接操作(setNativeProps)、調試
React Native學習筆記--進階(四)--導航器
React Native學習筆記--進階(五)--性能、升級、特定平臺代碼