本文中的内容基于react-navigation1.0.0-beta.11和react-native0.42版本实现。
概述
本文主要讲如何实现两个方面的内容:
- 在使用react-navigation做导航的应用中实现在登录页和Portal页连续点击两次物理返回按键退出应用的功能。
- 实现物理返回的效果和点击导航栏左上角的返回按钮的效果保持一致。物理返回包括Android设备点击物理返回按键返回以及Android和iOS均支持的手势右滑返回。
了解react-navigation的同学可能知道,navigation常用的方法一般有三个,navigate用于页面跳进,goBack用于页面返回,setParams用于向navigation赋属性或方法。
一般情况下,使用react-navigation做导航的应用中的大部分页面都会存在导航栏,也就是navigation中的header。我们可以在header中定义返回按键,并在点击返回按键时执行goBack方法,并且大多数情况下还会在执行goBack方法的同时执行必要的返回逻辑。
返回逻辑举例,比如:
- 根据当前页面的某些参数来决定要不要返回。
- 返回前调用上个页面传递进来的回调方法。
- 更复杂一点的,在调用上个页面传递进来的回调方法时,传递一些参数回去,例如在子页面中通过请求获取了一些数据,在返回时需要刷新上一页的页面内容。
核心原理
在react-navigation中有一个用于监听navigation变化的方法,叫做onNavigationStateChange。要实现上面提到的效果,都要围绕着这个监听方法来解决。首先来看一下这个方法的定义。
./react-navigation/src/createNavigationContainer.js
/.../
_onNavigationStateChange(
prevNav: NavigationState,
nav: NavigationState,
action: NavigationAction
) {
if (
typeof this.props.onNavigationStateChange === 'undefined' &&
this._isStateful()
) {
/* eslint-disable no-console */
if (console.group) {
console.group('Navigation Dispatch: ');
console.log('Action: ', action);
console.log('New State: ', nav);
console.log('Last State: ', prevNav);
console.groupEnd();
} else {
console.log('Navigation Dispatch: ', {
action,
newState: nav,
lastState: prevNav,
});
}
/* eslint-enable no-console */
return;
}
if (typeof this.props.onNavigationStateChange === 'function') {
this.props.onNavigationStateChange(prevNav, nav, action);
}
}
/.../
这个方法会接收到三个参数,分别是navigation发生变化之前的路由状态prevNav,navigation发生变化之后的路由状态nav以及发生的操作action。有了这三个参数,足够我们判断出每一次navigation发生变化时的前后状态以及发生了什么变化。
从NavigationActions的源码中可以得出,actions一共定义了六种操作。
action操作 | 说明 |
---|---|
Navigation/BACK | 执行goBack或物理返回时发生的动作 |
Navigation/INIT | 执行Navigator初始化时发生的动作 |
Navigation/NAVIGATE | 执行navigate跳进下一页时的动作 |
Navigation/RESET | 执行reset直接跳转到某一页时的动作 |
Navigation/SET_PARAMS | 执行setParams赋值或方法时的动作 |
Navigation/URI | 执行指定URI跳转时的动作 |
实现方式
基于这个方法,我们在应用最开始的StackNavigator上使用这个方法做监听,可以监听到应用中所有页面发生的跳转。然后我们来实现最开始提到的两个需求。
index.js
/.../
constructor(props) {
super(props);
this.state = {
firstTime: 0, //记录点击Android物理返回按键的时间
prevNav: null, //记录navigation发生变化之前的页面路由状态
nav: null, //记录navigation发生变化之后的页面路由状态
action: null, //记录发生的操作
setParams: [], //记录在跳进过程中,发生setParams操作的页面的action信息
};
}
componentDidMount() {
if(Platform.OS == 'android') {
BackAndroid.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid);
}
}
componentWillUnMount() {
if(Platform.OS == 'android') {
BackAndroid.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid);
}
}
onBackButtonPressAndroid = () => {
//进入引导页 or 进入登录页 or 进入Portal页 or 退回登录页 or 退回Portal页
if((this.state.action.type == 'Navigation/NAVIGATE' && this.state.action.routeName == 'guide') ||
(this.state.action.type == 'Navigation/NAVIGATE' && this.state.action.routeName == 'Login') ||
(this.state.action.type == 'Navigation/NAVIGATE' && this.state.action.routeName == 'portal') ||
(this.state.action.type == 'Navigation/RESET') ||
(this.state.action.type == 'Navigation/BACK' && this.state.nav.index == 2)) {
if(new Date().getTime() - this.state.firstTime > 2 * 1000) {
this.state.firstTime = new Date().getTime();
ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT, ToastAndroid.BOTTOM);
return true;
} else {
BackAndroid.exitApp();
}
}
return false;
}
onNavigationStateChange(prevNav, nav, action) {
if(action.type == 'Navigation/BACK') {
this.state.setParams[prevNav.index] && this.state.setParams[prevNav.index].params && this.state.setParams[prevNav.index].params.navigateBackPress && this.state.setParams[prevNav.index].params.navigateBackPress(true);
}
if(action.type == 'Navigation/SET_PARAMS') {
this.state.setParams[nav.index] = action;
}
this.state.prevNav = prevNav;
this.state.nav = nav;
this.state.action = action;
}
render() {
return (<MainPage onNavigationStateChange={this.onNavigationStateChange.bind(this)}></MainPage>);
}
/.../
这段代码可以定义在应用的入口页面中。
onBackButtonPressAndroid是Android设备点击物理返回按键时触发的方法。在这个方法中拿引导页、登录页和Portal举例,分别列举了进入和退回这三个页面时的情况。不管是进入这些页面还是退回到这些页面,当再次点击Android物理返回按键时,都应该直接提示"再按一次退出应用",这样就实现了我们的第一个需求。
onBackButtonPressAndroid方法返回true和false的含义是不同的,return true代表不执行返回上一页的动作,物理按键返回上一页的动作被截住。return false则执行返回上一页。
第二个需求实现的关键点在onNavigationStateChange方法中的两个if的使用。
先拿一个普通的带导航栏的页面做说明。
commonPage.js
/.../
export default class CommonScanResult extends Component {
static navigationOptions = ({
navigation,
screenProps
}) => ({
headerTitle: '普通页面',
headerLeft: (
<View style={styles.navBarRightButton}>
<TouchableOpacity style={styles.navBarRightButton_left} onPress={() => navigation.state.params.navigateBackPress(false)}>
<Image source={{ uri: GLOBAL.WebRoot + 'web/img/customer/back@2x.png' }} style={styles.backImage} />
<Text style={{ fontSize: 16, color: 'white', fontFamily: 'PingFangSC-Light' }}>返回</Text>
</TouchableOpacity>
</View>
),
});
constructor(props) {
super(props);
this.state = {
}
}
componentWillMount() {
}
componentDidMount() {
this.props.navigation.setParams({ navigateBackPress: this.navigateBackPress });
}
navigateBackPress = (isDeviceReturnKey) => {
this.props.navigation.state.params.onBack && this.props.navigation.state.params.onBack();
if(!isDeviceReturnKey) {
this.props.navigation.goBack();
}
}
render() {
return (
<View style={{ backgroundColor: '#f7f7f7', flex: 1, alignItems: 'center' }}>
</View>
);
}
}
/.../
commonPage就是一个带导航栏的最普通的页面,导航栏左侧是返回按钮。在componentDidMount中我们使用setParams为返回按钮定义了要执行的返回逻辑,除了需要调用goBack方法外还需要调用上个页面传来的回调方法onBack。如果我们的返回方式是直接点击了导航栏左侧的返回按钮,那么onBack和goBack都需要执行。但是当我们通过点击Android物理返回按键或者是手势右滑返回上一页,则goBack方法就不需要执行了,只需要执行返回时需要执行的其他逻辑就好。所以我们对navigateBackPress做了一些改进,让它接收一个参数isDeviceReturnKey,代表这次返回是不是通过物理返回实现的。当isDeviceReturnKey=true时,代表是物理返回。当isDeviceReturnKey=false时,代表是导航栏返回。那么现在的问题就是我们能在commonPage中调用到navigateBackPress并传false参数,那么怎么在index.js中调用到这个方法呢?
从刚才提到的那两个if逻辑中可以看出,action参数中存储了进入commonPage时的params,那在componentDidMount中执行的setParams也自然就把navigateBackPress存到了params中。所以在进入commonPage时将action存下来,等到要离开这个页面时在调用其中的回调方法执行返回时的逻辑就可以了。这种方式要求我们必须把页面的返回方法名称定义为navigateBackPress或者其他固定的名字。prevNav.index和nav.index分别是发生跳转前的页面路由层次和发生跳转后的页面路由层次。
setParams之所以被定义为数组,是因为页面发生的跳转有可能是连续的,比如连续的跳进再连续的跳出,所以我们需要将每一层的navigateBackPress都记下来,在哪一层返回就调用哪一层的navigateBackPress方法。
总结
通过以上方式,利用关键的两个方法onBackButtonPressAndroid和onNavigationStateChange,应用就可以实现在登录页和Portal页点击Android物理返回按键提示退出应用的效果,可以实现在某个页面通过物理返回方式返回上一页时的效果与点击导航栏左侧返回按钮的效果保持一致。