文章描述本人在开发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的一点心得体会,技术水平有限,如有发现不合理或不许确的地方,欢迎交流指正。