ReactNative填坑之旅–Navigation篇

桃子紅了吶發表於2017-11-07

React Native的導航有兩種,一種是iOS和Android通用的叫做Navigator,一種是支援iOS的叫做NavigatorIOS。我們這裡只討論通用的Navigator。會了Navigator,NavigatorIOS也就不是什麼難事了。

本文所使用的是React Native 0.34。FB團隊更新的太快了,我會在後續出現大的改動的時候更新本文以及程式碼。

Navigator基礎

Navigator在不同的Scene之間跳轉。

  • initialRoute物件
    這是Navigator所必須的,用於指定第一個Scene。

  • renderScene方法,這個方法必須。用flow的語法來描述的話是這樣的renderScene(router: any, navigator: Navigator)renderScene方法用來根據一個給定的route來繪製Scene。如:

    (route, navigator) => {
      <MySceneComponent title={route.title} navigator={navigator} />
    }
  • push方法,push(route: any)。Navigator使用這個方法跳轉到一個新的Scene。

API就瞭解這麼多,下面看一個簡單的例子。資料都是寫死的。

這個例子的主要功能就是從一個Scene(元件)HomeController,跳轉到另外的一個元件PetListController。就是從一組使用者裡點選一個之後顯示這個使用者擁有的寵物列表。

程式碼裡的User資料以及使用者的Pets資料都是寫死的。如果要學習網路請求方面的內容可以參考HomeController裡的fetchAction方法,以及填坑系列的前篇Http篇。

準備

HomeController,在這個元件裡顯示使用者列表。

import React, { Component } from `react`;
import {...略...} from `react-native`;

export default class HomeController extends Component {
    state: State;

    constructor(props) {
        super(props);

        const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
        this.state = {
            message: ``,
            dataSource: ds.cloneWithRows([`Micheal`, `Jack`, `Paul`])
        };
    }

// ...略...

    render() {
        return (
            <View style={{marginTop: 64}}>
                <ListView
                    dataSource={this.state.dataSource}
                    renderRow={this._renderRow.bind(this)}
                />
            </View>
        );
    }
};

文中儲備要程式碼都已經略去。

你可以看到,資料來源就是一個陣列[`Micheal`, `Jack`, `Paul`],裡面有三個人。資料最後顯示在ListView裡。

行渲染的時候,在行的裡面新增可以相應點選的TouchableHighlight,在使用者點選之後跳轉到PetListController中。

    _renderRow(data: string, sectionID: number, rowID: number, 
        highlightRow: (sectionID: number, rowID: number) => void) {
        return (
            <TouchableHighlight onPress={() => {
                    this._onPressRow(rowID);
                    highlightRow(sectionID, rowID);
                }}>
                <View style={styles.row}>
                    <Text style={styles.text}>{data}</Text>
                </View>
            </TouchableHighlight>
        );
    }

另外的一個PetListController裡只是顯示某個使用者的寵物列表。

export default class PetListController extends Component {
    state: State;

    constructor(props) {
        super(props);

        const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });

        this.state = {
            dataSource: ds.cloneWithRows([`Dog 1`, `Dog 2`, `Dog 3`])
        };
    }

// ...略...

    render() {
        return (
            <View style={{ marginTop: 64 }}>
                <ListView
                    dataSource={this.state.dataSource}
                    renderRow={this._renderRow.bind(this)}
                    renderSeperator={this._renderSeparator.bind(this)}
                    />
            </View>
        );
    }
};

在這個元件裡顯示的就是寵物資料。展示方式也是用的ListView

開始導航

在本例中,導航開始的地方不在某個具體的Controller裡(元件),而是在index.ios.js,android的在index.android.js裡。這麼做並不好,以後重構程式碼的時候會提升到同一個檔案中。

我們從Navigator繪製的地方開始導航的講解:

render() {
    return (
        <View style={styles.container}>
            <Navigator
                initialRoute={this.initialRoute}
                renderScene={this._renderScene}
                navigationBar={
                    <Navigator.NavigationBar
                        routeMapper={NavigationBarRouteMapper} />
                }
                />
        </View>
    );
}

回顧一下最開始的API,renderScene方法是用來繪製每一個Scene(場景)。Sene的實質就是一個個的元件,這個元件會佔滿一個螢幕。

元件的繪製需要有一些基本的資訊,這個資訊就是在initialRoute裡指定的。

    this.initialRoute = {
        title: `Users`,
        component: HomeController,
        index: 0,
        passProps: {
            // 在這裡傳遞其他的引數
        }
    }

這個initialScene是一個物件,內容有你自己定。

下面看看Scene的繪製方法renderScene

_renderScene(route: Route, navigator: Navigator) {
    if (route.component) {
        return React.createElement(route.component
            , {...this.props, ...route.passProps, navigator, route});
    }
}

這個方法每次都會返回一個ReactElement例項,和JSX語法返回的是一樣的,雖然JSX看起來是這樣的<HomeController />

這樣寫可能對於初學者來說有一點繞,那麼更加直觀一點的寫法是什麼樣呢?來看看:

_renderScene(route: Route, navigator: Navigator) {
    switch(route.index) {
        case 0: 
            return <HomeController />;
        case 1:
            return <PetListController />;
        default:
            return <HomeController />;
    }
}

這個寫法作用就是根據使用者當前要訪問的Route的index值來繪製相應的元件來作為當前的Scene。

但是,如此寫法也略顯複雜。你在寫這個render方法的時候需要知道全部的導航路勁,即從一開始是哪個Scene,第二部導航到哪個Scene,第三部。。。以此類推。在Navigator路徑上有幾個Scene就需要寫幾個。所以使用第一種寫法,用createElement方法來,根據指定的元件例項來返回作為Scene使用的元件。

NavigatorBar

上面的例子執行出來的時候有一個極大的問題,你不注意的話在PetListController沒法返回到HomeController。

介面上並沒有返回按鈕,但是RN居然把iOS的在最左側的手勢拖動返回上一級的功能實現了。這個功能在Android的實現上也有。總之隱藏不見的這個功能在使用者體驗上會有很大的問題。

所以必須用到Navigator的NavigationBar

<Navigator
  renderScene={(route, navigator) =>
    // ...
  }
  navigationBar={
     <Navigator.NavigationBar
       routeMapper={{
         LeftButton: (route, navigator, index, navState) =>
          { return (<Text>Cancel</Text>); },
         RightButton: (route, navigator, index, navState) =>
           { return (<Text>Done</Text>); },
         Title: (route, navigator, index, navState) =>
           { return (<Text>Awesome Nav Bar</Text>); },
       }}
       style={{backgroundColor: `gray`}}
     />
  }
/>

NavigatorBar裡設定了三個元素,左右兩個按鈕和中間的Title。上面程式碼中的按鈕無法響應使用者的點選操作。下面就看看如何新增這部分程式碼:

LeftButton: (route, navigator, index, navState) =>
  {
    if (route.index === 0) {
      return null;
    } else {
      return (
        <TouchableHighlight onPress={() => navigator.pop()}>
          <Text>Back</Text>
        </TouchableHighlight>
      );
    }
  },

理論上如的部分就看到這裡。我們看看我們的程式碼是怎麼新增的:

var NavigationBarRouteMapper = {
    LeftButton(route, navigator, index, navState) {
        if (index > 0) {
            return (
                <TouchableHighlight style={{ marginTop: 10 }} onPress={() => {
                    if (index > 0) {
                        navigator.pop();
                    }
                } }>
                    <Text>Back</Text>
                </TouchableHighlight>
            )
        } else {
            return null
        }
    },

    RightButton(route, navigator, index, navState) {
        return null;
    },

    Title(route, navigator, index, navState) {
        return (
            <TouchableOpacity style={{ flex: 1, justifyContent: `center` }}>
                <Text style={{ color: `white`, margin: 10, fontSize: 16 }}>
                    Data Entry
        </Text>
            </TouchableOpacity>
        );
    }
};

在左側按鈕中,首先檢查當前Scene的index是多少。如果是大於0的就說明可以回退到上一級,否則不作處理。

另外,給Title也新增了響應點選的程式碼。但是隻是一個效果,沒有新增onPress事件的處理程式碼。

push & pop

總結一下上面的內容。需要跳轉的HomeController和PetListController已經準備好了。導航用的Navigator也配置完成了,並且也包括NavigationBar。在繪製每一個Secne的時候,也給這些Scene傳入了props,裡面包含了Route物件和navigator物件。

有了上面的內容只是可以在執行起來的時候顯示第一個Scene:HomeController。於是,在HomeController的ListView裡的Row繪製的時候新增了TouchableHighLight並在相應事件裡呼叫了Navigator的push方法跳轉到下一個Scene。

    _onPressRow(rowID: number) {
        this.props.navigator.push({
            title: `Pets`,
            component: PetListController,
            passProps: {}
        });
    }

push方法裡傳入的物件就是Route型別(基本就是型別這個概念)。這個物件指明要跳轉的是哪個Scene,以及其他資訊。

pop方法在上面的NavigationBar裡的左側按鈕已經講到。

最後

要完全的實現Navigation,需要用到Navigator和跳轉的Scene(元件)。而把他們串聯起來的是Route定義和作為props傳入每個Scene的navigator物件。

程式碼

程式碼在這裡,可以同時支援Android和iOS。還沒有整理,不過對於這個簡單的例子來說正合適。

歡迎加群互相學習,共同進步。QQ群:iOS: 58099570 | Android: 572064792 | Nodejs:329118122 做人要厚道,轉載請註明出處!

本文轉自張昺華-sky部落格園部落格,原文連結:http://www.cnblogs.com/sunshine-anycall/p/5958012.html,如需轉載請自行聯絡原作者


相關文章