ReactNative-Navigator元件使用總結

風雨vs同舟發表於2017-12-27

文章描述本人在開發RN跨平臺應用時,使用Navigator導航器的一些實踐經驗,以防忘記,也供他人蔘考。

一、前言

如果你剛接觸reactNative,並且想跨平臺開發,可以直接選擇使用React Navigation。如果你只針對iOS平臺開發,並且想和iOS原生外觀一致,可以使用NavigatorIOS元件。

Navigator是官方推出的導航元件,相容iOS與Android兩端。從0.44版本開始,Navigator被從react native的核心元件庫中剝離到了一個名為react-native-deprecated-custom-components的單獨模組中。也就是說在0.44版本後,如果要使用Navigator,需要先將react-native-deprecated-custom-components安裝到工程中,在需要使用的地方import。

在實際開發過程中,Navigator效能表現還是比較不錯的,也很穩定,畢竟經歷了那麼多版本的檢驗。UI表現上也不錯,幾乎與原生相當。雖然被移除了RN核心庫,但不影響使用。

寫此文章時,我們團隊使用的RN是0.44.3版本,官方已經更新到0.51版本。

二、為什麼我不使用React Navigation

本來也是考慮直接使用React Navigation,但我們是在現有Native工程基礎上增加的RN功能,根據業務功能不同,分為不同的module。需要在同一個module中,根據Native不同的傳值,跳轉到不同的RN頁面,也就是說導航器的initialRoute(根檢視)是變化的,不是固定不變的。而經過研究,React Navigation比較適合根檢視是固定的情況,所以只好放棄之。

三、安裝&使用

1.安裝:

在專案根目錄執行命令(與node_modules同級):

npm install react-native-deprecated-custom-components
複製程式碼

注意:經過本人多次踩坑得到的經驗,安裝前,最好先執行命令npm install,再執行上面的安裝命令。直接安裝的話,會出現很多奇怪的問題 :broken_heart:。

安裝位置: 在~/node_modules/react-native-deprecated-custom-components目錄下

目錄截圖:

ReactNative-Navigator元件使用總結

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屬性,自己定義每個頁面的導航欄(比較煩,下面說)。

四、使用系統自帶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物件中有三個函式,LeftButtonRightButtonTitle,分別代表左邊按鈕,右邊按鈕和中間標題,它們的引數都是(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/>
                );
            }
        }
    },
複製程式碼

程式碼解釋:

  • 如果index > 0,表示當前頁面不是根檢視,返回按鈕基本上都是一個返回箭頭"<",或者"<返回",點選進行返回。 所以這裡定了一個TouchableOpacity按鈕,上面有一個Image。點選按鈕執行onPress時: ①如果某個頁面需要攔截返回事件,可以在其componentWillMount中給route定義一個backClick函式,進行攔截。程式碼如下:
componentWillMount(){
        this.props.route.backClick = () => {
            Keyboard.dismiss();
            const { navigator } = this.props;
            navigator.pop();
        };
    }
複製程式碼

②如果不需要攔截,則會直接執行navigator.pop(),開發者就不需要感知返回事件。

  • 如果index = 0,就判斷當前檢視的id是不是根檢視。如果是,同上也渲染一個按鈕,點選按鈕執行onPress時: ①如果某個頁面需要攔截返回事件,可以在其componentWillMount中給route定義一個rootBack函式,進行返回攔截。 ②如果不需要攔截,則會直接執行TRCNativeBridge.dismiss(),告訴Native關閉RN模組。

RightButton與TItle的原理與LeftButton類似,就不在贅述。

五、自定義navigationBar

由於使用系統自帶的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的方法。我的方法是:如果是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背景色一致。

八、防止快速點選多次push同一介面

在原生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的一點心得體會,技術水平有限,若有發現不合理或不準確的地方,歡迎交流指正。

原文連結

相關文章