文章描述本人在開發RN跨平臺應用時,使用Navigator導航器的一些實踐經驗,以防忘記,也供他人蔘考。
一、前言
如果你剛接觸reactNative,並且想跨平臺開發,可以直接選擇使用React Navigation。如果你只針對iOS平臺開發,並且想和iOS原生外觀一致,可以使用NavigatorIOS
元件。
Navigator
是官方推出的導航元件,相容iOS與Android兩端。從0.44版本開始,Navigator
被從react native的核心元件庫中剝離到了一個名為react-native-deprecated-custom-components
的單獨模組中。也就是說在0.44版本後,如果要使用Navigator
,需要先將react-native-deprecated-custom-components
安裝到工程中,在需要使用的地方import。
在實際開發過程中,Navigator
效能表現還是比較不錯的,也很穩定,畢竟經歷了那麼多版本的檢驗。UI表現上也不錯,幾乎與原生相當。雖然被移除了RN核心庫,但不影響使用。
寫此文章時,我們團隊使用的RN是0.44.3版本,官方已經更新到0.51版本。
二、為什麼我不使用React Navigation
本來也是考慮直接使用React Navigation
,但我們是在現有Native工程基礎上增加的RN功能,根據業務功能不同,分為不同的module。需要在同一個module中,根據Native不同的傳值,跳轉到不同的RN頁面,也就是說導航器的initialRoute(根檢視)是變化的,不是固定不變的。而經過研究,React Navigation
比較適合根檢視是固定的情況,所以只好放棄之。
三、安裝&使用
1.安裝:
在專案根目錄執行命令(與node_modules同級):
npm install react-native-deprecated-custom-components
複製程式碼
注意:經過本人多次踩坑得到的經驗,安裝前,最好先執行命令
npm install
,再執行上面的安裝命令。直接安裝的話,會出現很多奇怪的問題 :broken_heart:。
安裝位置: 在~/node_modules/react-native-deprecated-custom-components目錄下
目錄截圖:
2.使用:
Navigator
的使用與iOS中的UINavigationController類似,一般作為根檢視。將所有的子檢視元件都放到Navigator
中,再在對應render函式中返回Navigator
。
程式碼如下:
render() {
return (
<Navigator
initialRoute={{title: this._getRootConmponet().title, id: this.state.rootPageKey, component: this._getRootConmponet().component}}
configureScene={(route) => {
return Navigator.SceneConfigs.PushFromRight;
}}
renderScene={(route, navigator) => {
let Component = route.component;
return <Component {...route.params} route={route} navigator={navigator} />
}}
sceneStyle={{paddingTop: paddingTopOffset, paddingBottom:paddingBottomOffset}}
navigationBar={
<Navigator.NavigationBar
style={{
alignItems: 'center',
backgroundColor: '#f8f8f8',
borderBottomWidth:1/PixelRatio.get(),
borderBottomColor:'#cccccc',
}}
routeMapper={RouteMapper}
navigationStyles={Navigator.NavigationBar.Styles}
/>
}
/>
)
}
複製程式碼
程式碼解釋:
initialRoute
:為程式碼的根元件,也就是啟動app之後會看到介面的第一屏,其中,其有三個引數,title:元件的名字,id:是元件的唯一標識(字串型別,是為了區分元件的唯一性而自定義的),component:根元件。
initialRoute={{title: this._getRootConmponet().title, id: this.state.rootPageKey, component: this._getRootConmponet().component}}
複製程式碼
注:引數個數和引數名字不是固定的,你這裡怎麼定義,決定後面怎麼使用。
configureScene
:這個是場景配置,決定頁面之間跳轉時候的動畫方式,跳轉方式比較多,具體可以到NavigatorSceneConfigs.js檔案中檢視。
renderScene
:場景渲染,返回一個元件元素。let Component = route.component
就是取每個route裡的元件,例如initialRoute裡的component,在配置完後,return該元件。
sceneStyle
:場景樣式,統一設定頁面偏移量等,可以用來適配安卓和iOS導航欄高度不一致問題,也可以用來適配iPhoneX。
navigationBar
:導航欄屬性,返回一個Navigator.NavigationBar型別的元件,使用navigationBar屬性優點是方便,具有類似原生的過渡動畫;缺點是,需要在其屬性routeMapper中統一定製頁面導航欄樣式,而不能在各個頁面中定製導航欄樣式,如果有頁面的導航欄樣式比較特別,這就需要使用上文提到的元件id
進行判斷,耦合性比較高。當然也可以不設定navigationBar屬性,自己定義每個頁面的導航欄(比較煩,下面說)。
四、使用系統自帶navigationBar
1.navigationBar: 示例程式碼:
navigationBar={
<Navigator.NavigationBar
style={{
alignItems: 'center',
backgroundColor: '#f8f8f8',
borderBottomWidth:1/PixelRatio.get(),
borderBottomColor:'#cccccc',
}}
routeMapper={RouteMapper}
navigationStyles={Navigator.NavigationBar.Styles}
/>
}
複製程式碼
這裡navigationBar
只使用了三個屬性:
style
:統一定義navigationBar的樣式,背景色,底部線條,子檢視位置等。
routeMapper
:這個是navigationBar的靈魂,它決定navigationBar顯示什麼,如何操作等。
navigationStyles
:安卓和iOS的導航欄樣式不一樣,Navigator.NavigationBar.Styles
中會判斷當前是什麼系統,安卓就返回一個NavigatorNavigationBarStylesAndroid
,iOS就返回NavigatorNavigationBarStylesIOS
,後面提到的適配iPhoneX,就需要改動NavigatorNavigationBarStylesIOS
檔案。
2.routeMapper:
由於routeMapper
內容比較多,可以單獨抽出到一個js檔案中管理。
示例程式碼:
module.exports = {
//左邊按鈕
LeftButton(route, navigator, index, navState) {
if(index > 0) {
return (
<TouchableOpacity
onPress={() => {
if (route.backClick) {
route.backClick(); //如果動作被攔截,那就直接新動作
} else {
navigator.pop() //否則,pop
}
}}
style={styles.leftButtonStyle}>
<Image source={require('../images/trc_pay_pop_btn_back.png')} resizeMode='stretch'/>
</TouchableOpacity>
);
} else {
if (route.id === Config.AccountLoginPage.id) {
return (
<TouchableOpacity
onPress={() => {
if (route.rootBack) { //如果傳入根檢視返回,就執行新動作
return route.rootBack()
}else {
TRCNativeBridge.dismiss();
}
}}
style={styles.leftButtonStyle}>
<Image style={{marginLeft:10}}
source={require('../images/trc_account_login_close.png')}
resizeMode='stretch' />
</TouchableOpacity>
)
} else {
return (
<View/>
);
}
}
},
//右邊按鈕
RightButton(route, navigator, index, navState) {
if(index > 0 && route.rightButtonTitle) {
return (
<TouchableOpacity
onPress={() => {
if (route.rightBarButtonOnPress) { //道理同上
route.rightBarButtonOnPress()
}
}}
style={styles.rightButtonStyle}>
<Text style={styles.rightButtonTextStyle} numberOfLines={1}>{route.rightButtonTitle}</Text>
</TouchableOpacity>
);
} else {
return <View />
}
},
//標題
Title(route, navigator, index, navState) {
let title = route.title ? route.title : '';
return (
<View style={styles.titleBgStyle}>
<Text style={styles.middleButtonTextStyle}>{title}</Text>
</View>
);
}
};
複製程式碼
程式碼解釋:
routeMapper
物件中有三個函式,LeftButton
、RightButton
和Title
,分別代表左邊按鈕,右邊按鈕和中間標題,它們的引數都是(route, navigator, index, navState),它們都需要返回一個元件元素。
其中函式每個引數含義是:
route
:表示當前的路由。
navigator
:表示當前的導航器。
index
:表示當前的頁面的在導航棧中的位置索引。
navState
:表示當前的導航狀態。
3.LeftButton:
解釋一下LeftButton
:
//左邊按鈕
LeftButton(route, navigator, index, navState) {
if(index > 0) {
return (
<TouchableOpacity
onPress={() => {
if (route.backClick) {
route.backClick(); //如果動作被攔截,那就直接新動作
} else {
navigator.pop() //否則,pop
}
}}
style={styles.leftButtonStyle}>
<Image source={require('../images/trc_pay_pop_btn_back.png')} resizeMode='stretch'/>
{
iOS
?
<Text style={{marginLeft:-6, fontSize:accessoryFontSize}}>返回</Text>
:
null
}
</TouchableOpacity>
);
} else {
if (route.id === Config.AccountLoginPage.id) {
return (
<TouchableOpacity
onPress={() => {
if (route.rootBack) { //如果傳入根檢視返回,就執行新動作
return route.rootBack()
}else {
TRCNativeBridge.dismiss();
}
}}
style={styles.leftButtonStyle}>
<Image style={{marginLeft:10}}
source={require('../images/trc_account_login_close.png')}
resizeMode='stretch' />
</TouchableOpacity>
)
} else {
return (
<View/>
);
}
}
},
複製程式碼
程式碼解釋:
- 如果index > 0,表示當前頁面不是根檢視,返回按鈕基本上都是一個返回箭頭"<",或者"<返回",點選進行返回。
所以這裡定了一個
TouchableOpacity
按鈕,上面有一個Image
。點選按鈕執行onPress時: ①如果某個頁面需要攔截返回事件,可以在其componentWillMount中給route定義一個backClick
函式,進行攔截。程式碼如下:
componentWillMount(){
this.props.route.backClick = () => {
Keyboard.dismiss();
const { navigator } = this.props;
navigator.pop();
};
}
複製程式碼
②如果不需要攔截,則會直接執行navigator.pop()
,開發者就不需要感知返回事件。
- 如果index = 0,就判斷當前檢視的
id
是不是根檢視。如果是,同上也渲染一個按鈕,點選按鈕執行onPress時: ①如果某個頁面需要攔截返回事件,可以在其componentWillMount中給route定義一個rootBack
函式,進行返回攔截。 ②如果不需要攔截,則會直接執行TRCNativeBridge.dismiss()
,告訴Native關閉RN模組。
RightButton與TItle的原理與
LeftButton
類似,就不在贅述。
五、自定義navigationBar
由於使用系統自帶的navigationBar
,會增加程式碼耦合性,也不利於後期維護,所以只適合頁面導航欄定製化較少,功能比較簡單的專案。如果導航欄定製化較多,比如需要隱藏導航欄,導航欄上加搜尋框等功能時,使用自定義的navigationBar會比較好。
自定義導航欄,也就是寫一個公共的導航欄元件,定義好元件樣式,為各種情況提供屬性和事件callBack,在需要的頁面進行引用(幾乎每個頁面都需要 :flushed:)。 使用示例:
import NavigatorBar from './NavigatorBar';
export default class PageClass extends Component {
render() {
return (
<View style={{flex:1}}>
<NavigatorBar navigator={this.props.navigator}
title='SecretGarden'
hiddenLeftButton={true} />
</View>
)
}
}
複製程式碼
優點:真的freeStyle,想怎麼定製就怎麼定製。 缺點:①使用時比較煩,每次使用都要import;②過渡動畫不是很好。
六、適配iPhoneX
網上很多適配iPhoneX的方法。我的方法是:如果是iPhoneX,就把狀態列高度增加24畫素,也就是在上面說到的NavigatorNavigationBarStylesIOS
檔案中,修改STATUS_BAR_HEIGHT,如下:
var STATUS_BAR_HEIGHT = 20 + (Dimensions.get('window').height === 812 ? 24 : 0); //change by meng, note:適配iPhone X
複製程式碼
上面只是把狀態列增高,但是還需要將頁面頂部向下偏移24畫素,頁面底部向上偏移34畫素。注意:安卓不需要偏移。
//頂部偏移
const iOSPaddingTop = 64 + (SCREEN_HEIGHT === 812 ? 24 : 0); //適配iPhone X
const androidPaddingTop = 56;
const paddingTopOffset = global.Android ? androidPaddingTop : iOSPaddingTop;
//底部偏移
const iOSPaddingBottomOffset = SCREEN_HEIGHT === 812 ? 34 : 0; //適配iPhone X
const androidPaddingBottomOffset = 0;
const paddingBottomOffset = global.Android ? androidPaddingBottomOffset : iOSPaddingBottomOffset;
複製程式碼
這樣就完成了iPhoneX的適配。
七、適配安卓沉浸式
安卓沉浸式不是屬於Navigator部分,但一般討論導航欄都會與狀態列聯絡起來,所以在此順便說一下。
沉浸式是安卓5.0系統上的新功能。即5.0以上系統,可以設定狀態列透明,頁面佈局從狀態列頂部開始。 5.0以下系統,狀態列是黑底白字。
適配方法如下:
import { StatusBar } from 'react-native';
componentWillMount() {
if (Android) {
StatusBar.setBackgroundColor('#f8f8f8');
StatusBar.setBarStyle('dark-content', true);
}
}
複製程式碼
其中背景色設為與navigationBar
背景色一致。
八、防止快速點選多次push同一介面
在原生iOS上,經常會遇到快速點選一次按鈕,同一個頁面會push出兩次或多次,在RN上也會有這個問題。我的解決方法是:修改Navigator
導航器原始碼,在Navigator
進行push的時候,判斷要push的頁面與當前棧頂的頁面的id
是不是相同,如果不相同,就push;如果相同,就return。
程式碼如下:
push: function(route) {
//----------【修改原始碼開始】change by meng----------
const currentRoutes = this.getCurrentRoutes();
if (currentRoutes.length > 0) {
let lastRoute = currentRoutes[currentRoutes.length - 1];
let oldId = lastRoute.id;
let newId = route.id;
if (oldId && newId && oldId === newId) {
//如果是連續push到同一個頁面,就直接返回
return;
}
}
//----------【修改原始碼結束】----------
...
}
複製程式碼
以上就是我在開發過程中使用Navigator的一點心得體會,技術水平有限,若有發現不合理或不準確的地方,歡迎交流指正。