文章描述本人在開發RN跨平臺應用時,使用Navigator導航器的一些實踐經驗,以防忘記,也供他人蔘考。javascript
若是你剛接觸reactNative,而且想跨平臺開發,能夠直接選擇使用React Navigation。若是你只針對iOS平臺開發,而且想和iOS原生外觀一致,可使用NavigatorIOS
組件。java
Navigator
是官方推出的導航組件,兼容iOS與Android兩端。從0.44版本開始,Navigator
被從react native的核心組件庫中剝離到了一個名爲react-native-deprecated-custom-components
的單獨模塊中。也就是說在0.44版本後,若是要使用Navigator
,須要先將react-native-deprecated-custom-components
安裝到工程中,在須要使用的地方import。node
在實際開發過程當中,Navigator
性能表現仍是比較不錯的,也很穩定,畢竟經歷了那麼多版本的檢驗。UI表現上也不錯,幾乎與原生至關。雖然被移除了RN核心庫,但不影響使用。react
寫此文章時,咱們團隊使用的RN是0.44.3版本,官方已經更新到0.51版本。android
原本也是考慮直接使用React Navigation
,但咱們是在現有Native工程基礎上增長的RN功能,根據業務功能不一樣,分爲不一樣的module。須要在同一個module中,根據Native不一樣的傳值,跳轉到不一樣的RN頁面,也就是說導航器的initialRoute(根視圖)是變化的,不是固定不變的。而通過研究,React Navigation
比較適合根視圖是固定的狀況,因此只好放棄之。shell
1.安裝:npm
在項目根目錄執行命令(與node_modules同級):react-native
npm install react-native-deprecated-custom-components
複製代碼
注意:通過本人屢次踩坑獲得的經驗,安裝前,最好先執行命令
npm install
,再執行上面的安裝命令。直接安裝的話,會出現不少奇怪的問題 :broken_heart:。app
安裝位置: 在~/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屬性,本身定義每一個頁面的導航欄(比較煩,下面說)。
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/>
);
}
}
},
複製代碼
代碼解釋:
TouchableOpacity
按鈕,上面有一個Image
。點擊按鈕執行onPress時: ①若是某個頁面須要攔截返回事件,能夠在其componentWillMount中給route定義一個backClick
函數,進行攔截。代碼以下:componentWillMount(){
this.props.route.backClick = () => {
Keyboard.dismiss();
const { navigator } = this.props;
navigator.pop();
};
}
複製代碼
②若是不須要攔截,則會直接執行navigator.pop()
,開發者就不須要感知返回事件。
id
是否是根視圖。若是是,同上也渲染一個按鈕,點擊按鈕執行onPress時: ①若是某個頁面須要攔截返回事件,能夠在其componentWillMount中給route定義一個rootBack
函數,進行返回攔截。 ②若是不須要攔截,則會直接執行TRCNativeBridge.dismiss()
,告訴Native關閉RN模塊。RightButton與TItle的原理與
LeftButton
相似,就不在贅述。
因爲使用系統自帶的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,就把狀態欄高度增長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
背景色一致。
在原生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的一點心得體會,技術水平有限,如有發現不合理或不許確的地方,歡迎交流指正。