react-navigation实现物理返回与导航栏返回的良好兼容方式

本文中的内容基于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物理返回按键提示退出应用的效果,可以实现在某个页面通过物理返回方式返回上一页时的效果与点击导航栏左侧返回按钮的效果保持一致。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343