ReactNative系列(五):react-natigatio
ReactNative整理:《ReactNative系列》
内容目录
1、navigationOptions
和AppContainer
2、导航器属性参数
3、StackNavigator
用法详解
4、SwitchNavigator
用法详解
5、BottomTabNavigator
用法详解
6、DrawerNavigator
用法详解
7、结语
一、navigationOptions
和AppContainer
AppContainer
:负责管理应用的state
并将顶层的navigator
链接到整个应用环境。创建各种navigator
时,已经将要用到的页面整合到一起,并生成了一个导航组件,但是该组件并没有接入到应用中,所以需要AppContainer
将导航组件包裹,同时链接到整个应用环境,这样生成的导航器就可以使用了。
1、关于createAppContainer
的简单示例:
import { createAppContainer, createStackNavigator } from 'react-navigation';
const StackControllers = createStackNavigator({
// 路由配置对象
}, {
// 导航属性配置
});
// 将生成的导航器组件 StackControllers 包裹到 Container 中
const StackContainer = createAppContainer(StackControllers);
// 现在 StackContainer 变成了 React 渲染的主要组件
export default StackContainer;
2、React Native 中的 createAppContainer
prop:
<StackContainer
onNavigationStateChange={this.handleNavigationChange()}
uriPrefix={'/app'}
/>
-
onNavigationStateChange(prevState, newState, action)
每当导航器管理的navigation state 发生变化时,都会调用该函数。它接收之前的 state、navigation 的新 state 以及发布状态更改的 action。 默认情况下,它将 state 的更改打印到控制台。 -
uriPrefix
应用可能会处理的URI前缀,在处理深度链接以提取传递给路由器的路径时使用。
navigationOptions
:导航器内部页面的选项配置。可以在导航器RouteConfigs
和NavigatorConfig
中配置;也可以在页面中配置。优先级为:RouteConfigs
配置 > 页面中navigationOptions
配置 > NavigatorConfig
配置。
以StackNavigator为例:
// 导航器中配置
const StackControllers = createStackNavigator({
stack1: {
screen: StackController1,
navigationOptions: () => ({
title: 'controller1'
})
},
stack2: {
screen: StackController2
}
}, {
initialRouteName: 'stack1',
});
// 页面中配置 navigationOptions
export default class StackController1 extends Component {
static navigationOptions = {
title: 'StackController1'
};
...
}
如果RouteConfigs
中和页面中都存在navigationOptions
,则以RouteConfigs
中的配置为准。
navigationOptions
是用来配置页面头部或者手势等属性的,RouteConfigs
和页面中静态配置,是针对单个页面的;而在XXNavigatorConfig
中配置,则是针对导航内所有screen
生效。
二、 导航器属性参数
1、Navigation prop reference
应用中的每个页面组件都会自动提供navigation prop
,该属性包含便捷的方法用于触发导航操作,如下所示:
-
this.props.navigation
-
navigate
- 跳转到另一个屏幕,计算出需要执行的操作 -
goBack
- 关闭活动屏幕并在堆栈中向后移动 -
addListener
- 订阅导航生命周期的更新 -
isFocused
- 如果屏幕获取焦点,函数返回true
,否则返回false
-
state
- 当前state
,路由状态 -
setParams
- 更改路由的参数 -
getParam
- 获取具有回退功能的特定参数 -
dispatch
- 向路由发送action -
dangerouslyGetParent
- 返回父级navigator的函数
-
重点是要强调navigation
属性不会传递给所有组件;只有screen
页面组件会自动收到此属性。
2、Navigator-dependent functions
this.props.navigation
上有些取决于当前navigator的附加函数
如果是StackNavigator
,除了navigate
和goBack
,还提供了如下方法:
-
this.props.navigation
-
push
- 推一个新的路由到堆栈 -
pop
- 返回堆栈中的上一个页面 -
popToTop
- 跳转到堆栈中最顶层的页面 -
replace
- 用新路由替换当前路由 -
reset
- 擦除整个导航状态,并将其替换为多个操作的结果 -
dismiss
- 关闭当前堆栈
-
如果是DrawerNavigator
,则还可以使用以下选项:
-
this.props.navigation
-
openDrawer
- 打开 -
closeDrawer
- 关闭 -
toggleDrawer
- 切换,如果是打开则关闭,反之亦然
-
三、StackNavigator
用法详解
堆栈式导航
:提供一种在每个新屏幕放置在堆栈顶部的屏幕之间转换的方法。该导航器是以栈的形式管理页面,每新建一个页面都会压入栈中,最新创建的页面在栈顶。默认情况下的配置具有熟悉的Android和iOS外观&效果:iOS上从右侧滑入,Android上从底部淡入。
StackNavigator配置代码示例:
/**
* 堆栈导航:
* 将页面配置到导航器中,不能跳转到导航没有配置的页面
* @type {NavigationContainer}
*/
const StackControllers = createStackNavigator({
// RouteConfig 配置
stack1: {
screen: StackController1,
navigationOptions: {
title: 'Controller1',
headerStyle: {
backgroundColor: '#ffffff'
}
}
},
stack2: {
screen: StackController2,
navigationOptions: {
title: '页面2'
}
},
stack3: {
screen: StackController3
},
stack4: {
screen: StackController4
}
}, { //
initialRouteName: 'stack1',
defaultNavigationOptions: {
headerStyle: {
backgroundColor: 'grey',
},
headerTintColor: 'blue',
headerTitleStyle: {
fontSize: 20,
},
}
});
const StackContainer = createAppContainer(StackControllers);
export default StackContainer;
RouteConfig
- 配置的页面必须含有screen
属性值,用来定义页面标识;navigationOptions
用来初始化页面的一些配置,例如:Header样式,手势等。
需要注意的是:
StackNavigatorConfig
- 配置中用的是defaultNavigationOptions
控制导航内所有页面Header展示;用navigationOptions
配置没有效果,react-navigation
版本号是3.8.1,各位同学可以自己尝试下。
initialRouteName
属性值是配置导航器的默认页面;在没有设置的情况下,默认为RouteConfig
中配置的第一个页面。
页面代码:
/**
* 展示页面
* 跳转方法:handleOnPress
* 返回方法:backPress
*/
export default class StackController2 extends Component {
constructor(props) {
super(props);
this.handleOnPress = this.handleOnPress.bind(this);
}
componentDidMount() {
console.log('-did-mount-stack2--');
}
componentWillUnmount() {
console.log('-un-mount-stack2--');
}
/* 点击跳转到第三个页面 stack3 */
handleOnPress() {
this.props.navigation.navigate('stack3');
}
/* 点击返回上层页面 */
backPress() {
this.props.navigation.goBack();
}
render() {
return(
<View style={pageStyle.container}>
<Text
style={pageStyle.contentText}
onPress={this.handleOnPress}
>
Controller2 To Controller3
</Text>
<Text
style={pageStyle.backText}
onPress={this.backPress}
>
返回
</Text>
</View>
);
}
}
看过ReactNative
官方文档的同学应该知道:
常用的点击事件组件有TouchableOpacity
、Button
和Text
,它们都包含onPress
属性,可以调用点击方法。我这里用的是Text
组件实现点击切换页面和页面返回。
StackNavigator
的页面创建与跳转相对比较简单,比较麻烦的是多层页面的关闭。这里给出几种多层页面退出的解决办法:
例如:从A -> B -> C -> D页面,要从D返回到A
1、利用页面key
值
导航器中每个页面都包含navigation
属性值,可以通过this.props.navigation
取到,该属性中有许多方法和数据,其中包括state
。在state
中包含key
、routeName
和params
:key
- 是页面在导航器中的唯一标识ID,根据这个标识能找到对应页面;routeName
- 是当前页面在导航器中配置的路由名称;params
- 传递的参数,是由上一个页面传入。
注意:从D返回到A,用到的是B页面的key
值,而不是A的。
/**
* 点击跳转到第三个页面 stack3
* 其中 B: navigate.state.key 为传递的参数
*/
handleOnPress() {
const navigate = this.props.navigation;
navigate.navigate('stack3', {
B: navigate.state.key
});
}
可以用类似的代码结构做出四个页面,测试跳转和返回。
/**
* 最后一个页面的点击事件
* 点击返回到A页面
*/
handleOnPress() {
const navigate = this.props.navigation;
navigate.goBack(navigate.state.params.B);
}
这样,在点击最后一个页面的文本时,就能返回到A页面,而且没有多余的退栈动画。
2、拦截路由action
和state
改变
由createStackNavigator
关联到的源码:创建时传递两个参数生成的是NavigationContainer
,而该NavigationContainer是个接口,包含router
属性。继续向下查看源码发现router
的value值是NavigationRouter
,其中有导航调用的方法,getStateForAction
就是我们需要用到的,它能监听交互的action
和导航的state
。下面摘出来源码:
...
// 堆栈导航的创建
export function createStackNavigator(
routeConfigMap: NavigationRouteConfigMap,
stackConfig?: StackNavigatorConfig
): NavigationContainer;
...
// NavigationContainer接口及属性
export interface NavigationContainer extends React.ComponentClass<
NavigationContainerProps & NavigationNavigatorProps<any>
> {
new (
props: NavigationContainerProps & NavigationNavigatorProps<any>,
context?: any
): NavigationContainerComponent;
router: NavigationRouter<any, any>;
screenProps: ScreenProps;
navigationOptions: any;
state: { nav: NavigationState | null };
}
// NavigationRouter -- 导航路由接口
export interface NavigationRouter<State = NavigationState, Options = {}> {
/**
* The reducer that outputs the new navigation state for a given action, with
* an optional previous state. When the action is considered handled but the
* state is unchanged, the output state is null.
*/
getStateForAction: (
action: NavigationAction,
lastState?: State
) => State | null;
/**
* Maps a URI-like string to an action. This can be mapped to a state
* using `getStateForAction`.
*/
getActionForPathAndParams: (
path: string,
params?: NavigationParams
) => NavigationAction | null;
getPathAndParamsForState: (
state: State
) => {
path: string;
params?: NavigationParams;
};
getComponentForRouteName: (routeName: string) => NavigationComponent;
getComponentForState: (state: State) => NavigationComponent;
/**
* Gets the screen navigation options for a given screen.
*
* For example, we could get the config for the 'Foo' screen when the
* `navigation.state` is:
*
* {routeName: 'Foo', key: '123'}
*/
getScreenOptions: NavigationScreenOptionsGetter<Options>;
}
了解依据之后,来修改我们自己的代码:
// 定义拦截器,用来将修改的action和state作为新的数据传入
const StackInterceptor = StackControllers.router.getStateForAction;
/**
* 拦截思路:
* 1、过滤action,只有是 action.type === 'Navigation/BACK' 时拦截处理
* 2、根据拦截到的action中的key值,在state.routes中找到对应数据
* 3、找到的对应的route数据下标index,取 index + 1 的数据的key值赋给 action.key
* 4、把新修改的 action 和 state 传入定义好的拦截器中
*/
StackControllers.router.getStateForAction = (action, state) => {
console.log(action, '---action--');
console.log(state, '---state--');
let nextAction = action;
if (state && action && action.type === 'Navigation/BACK') {
const routeLength = state.routes.length;
const isExist = state.routes.findIndex(route => action.key === route.routeName);
if (isExist > -1 && isExist + 1 <= routeLength) {
nextAction = {
...action,
key: state.routes[isExist + 1].key
};
}
console.log(nextAction, '--nextAction--');
console.log(state, '--nextstate---');
}
return StackInterceptor(nextAction, state);
};
该方法中,我们用到的goBack
方法能直接传递导航器中配置的路由名称为参数。例如:this.props.navigation.goBack('stack1')
。同样的,返回A页面用到的还是B页面的Key值,所以在拦截查找位置的时候要取到stack1
的下标index
的下一个位置index + 1
的数据才行。
依据同样的方法,可以通过修改state.routes
的数据来实现由D -> A,我们通过日志可以看到 B、C、D三个页面的componentWillUnmount
方法都没有执行,虽然页面仍然能创建跳转和关闭,但是毕竟影响了组件的生命周期,所以不推荐大家使用。修改action.key
和state.routes
都能达到相同的效果,但建议使用前者更稳妥。
另:
其实还可以通过修改源码方法实现多层页面返回。3.x版本之前可以通过修改react-navigation
源码的StackRouter.js
中针对action.type === NavigationActions.BACK
修改返回方式;但是3.x之后版本光修改该文件不生效了,还需要修改别的地方(还在摸索)。而且开发中会碰到有些问题需要删除node_modules
文件夹重新npm install
的情况,这时node_modules
文件夹会重置,需要再重新修改react-navigation
源码,很麻烦,所以建议大家不要修改源码。
四、SwitchNavigator
用法详解
SwitchNavigator
的用途是一次只显示一个页面。 默认情况下,它不处理返回操作,并在你切换时将路由重置为默认状态。项目中我们时常会碰到进入应用时展示启动页、广告页或者校验身份的需求,而且这些页面展示一次后就不再返回。此时就可以用SwitchNavigator
来实现。
const SwitchControllers = createSwitchNavigator({
switch1: { // 广告页面或者身份验证页面
screen: SwitchController1
},
switch2: { // 主页面
screen: SwitchController2
}
}, {
initialRouteName: 'switch1',
resetOnBlur: true,
backBehavior: 'none'
});
export default SwitchContainer = createAppContainer(SwitchControllers);
SwitchNavigator
单独使用局限性比较大,往往适合与别的导航器嵌套使用:比如,示例中的SwitchController1和SwitchController2都可以用其他类型导航器代替,其中可以包含多个页面。
需要注意的属性有两个:
-
resetOnBlur
- 用来标识切换离开屏幕时是否需要重置所有嵌套的导航器状态,默认值是true
。 -
backBehavior
- 设置后退按钮是否会导致标签切换到初始路由,如果是,设置为initialRoute
;否则为none
。默认是none
。
SwitchNavigator
可以实现切换路由后,返回键不能回到上一个页面的功能,在某些特定情况下可以使用该特质。
五、BottomTabNavigator
用法详解
TabNavigator
标签导航是我们最常见的一种导航样式,在3.x中将TabNavigator
被移除,改用BottomTabNavigator
和MaterialTopTabNavigator
,两者类似。这里只对前者进行讲解。
还是和其他导航器创建一样,需要两个参数配置对象。
const tab_home_select = require('../../../resource/tabbar_home_select.png');
const tab_home = require('../../../resource/tabbar_home.png');
const tab_list_select = require('../../../resource/tabbar_list_select.png');
const tab_list = require('../../../resource/tabbar_list.png');
const tab_self_select = require('../../../resource/tabbar_self_select.png');
const tab_self = require('../../../resource/tabbar_self.png');
const BottomTabControllers = createBottomTabNavigator({
Home: {
screen: TabHomeController,
navigationOptions: {
title: '首页',
tabBarLabel: '首页',
tabBarIcon: ({ focused, horizontal, tintColor }) => (
<Image
source={focused ? tab_home_select : tab_home}
style={{ width: 20, height: 20 }}
resizeMode={'contain'}
/>
)
}
},
List: {
screen: TabListController,
navigationOptions: {
title: '书单',
tabBarLabel: '书单',
tabBarIcon: ({ focused, horizontal, tintColor }) => (
<Image
source={focused ? tab_list_select : tab_list}
style={{ width: 20, height: 20 }}
resizeMode={'contain'}
/>
),
// tabBarOnPress: ({navigation, defaultHandler}) => {
// console.log(navigation, '--navigation--');
// console.log(defaultHandler, '--defaultHandler--');
// }
}
},
Self: {
screen: TabMineController,
navigationOptions: {
title: '我的',
tabBarLabel: '我的',
tabBarIcon: ({ focused, horizontal, tintColor }) => (
<Image
source={focused ? tab_self_select : tab_self}
style={{ width: 20, height: 20 }}
resizeMode={'contain'}
/>
)
}
}
}, {
lazy: true,
initialRouteName: 'Home',
order: ['Home', 'List', 'Self'],
tabBarOptions: {
activeTintColor: '#FF8800',
inactiveTintColor: '#666666',
showIcon: true,
labelStyle: {
fontSize: 12
},
style: {
backgroundColor: 'white',
height: 45
}
}
});
有几个比较重要的属性这里需要提一下:
-
tabBarIcon
- 该方法中有三个参数:-
focused
- 当前Tab是否获取焦点,如果是我们一般都会设置当前Tab高亮; -
horizontal
- 当前是否横屏,如果横屏为true
,否则为false
; -
tintColor
- 对应activeTintColor
和inactiveTintColor
,如果获取焦点则为activeTintColor
设置的rgba
色值字符串,否则为inactiveTintColor
;如果没有设置则返回默认色值。
-
-
lazy
- 是否懒加载。和原生一样,懒加载除了能提高渲染性能之外,还可以提升交互体验。 -
order
- 底部Tab 的位置,是在左侧还是中间,都可以通过这个属性调整。 -
tabBarOptions
- 设置TabBar的一些属性:激活与非激活状态下的颜色、是否显示图标或者图标文本样式等。
有个特殊属性可以对Tab的点击进行监测:
tabBarOnPress
- 用来添加自定义逻辑处理,该方法在切换到下一个页面之前调用,包含的参数可以直接用一个event
表示,或者可以拆分成navigation
和defaultHandler
。
方法中需要调用defaultHandler()
,否则会页面切换失效。
// 两个参数navigation、defaultHandler
tabBarOnPress: ({navigation, defaultHandler}) => {
defaultHandler();
}
// 一个event参数
tabBarOnPress: (event) => {
event.defaultHandler();
}
六、DrawerNavigator
用法详解
DrawerNavigator
抽屉式导航也是常见导航类型之一,原生应用中经常会见到 -- 由侧滑菜单来控制页面跳转。但是一个应用肯定会有不少页面,如果都用侧滑菜单来控制的话,不仅混乱,而且体验也不是很好,所以抽屉式导航往往是与别的导航器嵌套使用的。
const DrawerControllers = createDrawerNavigator({
Main: {
screen: DrawerMainController,
},
List: {
screen: DrawerListController,
},
Self: {
screen: DrawerSelfController,
},
Setting: {
screen: DrawerSettingController,
}
}, {
drawerWidth: 300,
drawerPosition: 'left',
initialRouteName: 'Main',
order: ['Main', 'List', 'Self', 'Setting'],
drawerLockMode: 'locked-closed',
drawerType: 'slide',
contentComponent: (props) => {
console.log(props, '--props--');
return (
<ScrollView style={{flex: 1}}>
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
<DrawerItems {...props}/>
</SafeAreaView>
</ScrollView>
);
},
contentOptions: {
activeTintColor: '#FF8800',
inactiveTintColor: '#666666',
}
});
export default DrawerContainer = createAppContainer(DrawerControllers);
其中有几个比较重要的属性需要注意:
-
drawerIcon
- 侧滑item的图标,在页面的各自navigationOptions中配置。会回传两个参数:focused
状态是否选中标识;tintColor
item选中时的色值。 -
drawerLockMode
- 设置抽屉的锁定模式:unlocked
,是默认值,用手势可以打开和关闭抽屉;locked-closed
,锁定关闭,在抽屉保持关闭的状态下,用手势不能打开;locked-open
,锁定打开,在抽屉打开的状态下,用手势不能关闭抽屉。 -
contentComponent
- 该属性是用来设置侧滑内容组件的,可以自定义组件样式,默认情况下为DrawerItems(该组件可以从react-navigation中导入)。方法中会传递props
属性给item组件,通过打印可以查看里面包含的值以及方法(选其中几个,大部分比较好理解):-
activeItemKey
- 是当前选中页面的key值标签 -
items
- 抽屉的路由数组,可以修改或覆盖。其中元素为页面路由对象state
值,包含key
、routeName
和params
三个参数 -
descriptors
- 我理解为描述元,里面包含抽屉页面的常用属性值,例如:key
、navigation
、state
和options
(这个不知道具体用处)
-
-
contentOptions
- 内容选项,用来设置item的属性值。-
activeTintColor
- 当前选项卡的标签和图标颜色 -
inactiveTintColor
- 非当前选项卡的标签和图标颜色 -
onItemPress
- 当item被点击时调用 -
itemStyle
- 子组件item的样式
-
七、结语
以上是对几个导航器的拆分理解,其中的属性只是挑出了一部分,比较重要、难理解或者典型的,并不是全部。剩余的需要大家自己去对照上一篇尝试或者log日志输出对比,这里就不再挨个讲解了。相信自己动手尝试过的肯定要记忆更深,理解也更透彻。
单个导航理解以后,它们的优缺点也就有了基本的认识,之后就是各种搭配组合使用了。多个导航嵌套能实现更复杂的业务需求,也能提高交互体验。
下一篇:ReactNative系列(六):react-natigation 3.x全解(下)
如果有不对的地方欢迎指出,大家互相讨论,如果喜欢请点赞关注