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/[email protected]' }} 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=true時,代表是導航欄返回。那麼現在的問題就是我們能在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物理返回按鍵提示退出應用的效果,可以實現在某個頁面通過物理返回方式返回上一頁時的效果與點選導航欄左側返回按鈕的效果保持一致。