React Native使用SectionList打造城市選擇列表,包含分組的跳轉

1.背景

本文使用的RN(使用RN表示React Native)的版本為0.44版本,從官方文檔上看SectionList是從0.43版本才有的,而要列表的吸頂懸浮功能,從0.44版本才開始有。具體可以查看React Native的官方文檔。至于為何使用SectionList而不是使用ListView,可以自行百度SectionList(FlatList)的好處,就我了解主要是性能上的差別(這點深受ListView其害)。這里就不加以討論了,本文主要介紹的是如何使用SectionList打造分組懸停,并且添加右側的分組的跳轉控制(類似微信的通訊錄)

2.SectionList簡單介紹

首先我們要做的是,如何使用SectionList生成一個分組列表,具體的使用我們可以看下RN官方中文文檔http://reactnative.cn/docs/0.44/sectionlist.html 其中對SectionList和FlatList的介紹,我們來看看數據源的格式

<SectionList
    renderItem={({item}) => <ListItem title={item.title} />}
    renderSectionHeader={({section}) => <H1 title={section.key} />}
    sections={[ // 不同section渲染相同類型的子組件
        {data: [{key:...}...], key: ...},
        {data: [{key:...}...], key: ...},
        {data: [{key:...}...], key: ...},
]}
/>

在這里我們重點要注意的是,對于每組數據,必須有一個key的字段,并且不同組之間key的內容必須是不一樣的,同時每組里面的數據也必須有一個key的字段,key的內容同樣是不一致的。(雖然我也不曉得為嘛要這樣設計,但是實際使用過程中不添加的話的確會報warning,有興趣的話可以看下其中的源碼)

3.SectionList生成城市列表

3.1 格式化城市列表數據

這里我們有一個city.json的數據源,里面包含了國內的城市列表信息,格式組成如下

{
    "data": [
        {
            "title": "A"
            "city": [
                {
                    "city_child": "阿壩",
                    "city_child_en": "aba",
                    "city_id": 101271901,
                    "city_name_ab": "ab.ab",
                    "city_parent": "阿壩",
                    "city_pinyin_name": "aba.aba",
                    "country": "中國",
                    "latitude": 32,
                    "longitude": 101,
                    "provcn": "四川"
                },
                {
                    "city_child": "阿巴嘎",
                    "city_child_en": "abaga",
                    "city_id": 101080904,
                    "city_name_ab": "xlgl.abg",
                    "city_parent": "錫林郭勒",
                    "city_pinyin_name": "xilinguole.abaga",
                    "country": "中國",
                    "latitude": 44,
                    "longitude": 114,
                    "provcn": "內蒙古"
                },
                 .....]
        },
        .......
      ]
}

現在我們要做的就是把這樣的數據源格式化成我們SectionList需要的數據的格式,格式化代碼如下

 async getCityInfos() {
        let data = await require('../app/assets/city.json');
        let jsonData = data.data
        //每組的開頭在列表中的位置
        let totalSize = 0;
        //SectionList的數據源
        let cityInfos = [];
        //分組頭的數據源
        let citySection = [];
        //分組頭在列表中的位置
        let citySectionSize = [];
        for (let i = 0; i < jsonData.length; i++) {
            citySectionSize[i] = totalSize;
            //給右側的滾動條進行使用的
            citySection[i] = jsonData[i].title;
            let section = {}
            section.key = jsonData[i].title;
            section.data = jsonData[i].city;
            for (let j = 0; j < section.data.length; j++) {
                section.data[j].key = j
            }
            cityInfos[i] = section;
            //每一項的header的index
            totalSize += section.data.length + 1
        }
        this.setState({data: cityInfos, sections: citySection, sectionSize: citySectionSize})
    }

在這里我們用async async,然后異步讀取city.json里面的數據,然后遍歷整個數據我們得到三組我們需要的數據,分別為SectionList的數據源(列表展示使用),分組頭的數據源(后面我們在右側展示是使用),分組頭在列表中的位置(做列表跳轉的時候使用)。
??這樣我們得到了數據源,然后將其添加到SectionList中,我們來看下效果

 <SectionList
   ref='list'
   enableEmptySections
   renderItem={this._renderItem}
   renderSectionHeader={this._renderSectionHeader}
   sections={this.state.data}
   getItemLayout={this._getItemLayout}/>
1.jpeg

3.2 列表右側分組頭展示

在上面中我們得到了分組的頭的列表citySection,那么我們改如和將其顯示到列表右側呢?
??在這里我們將頭部使用Text進行展示,然后外部使用View進行包裹,對外部的View進行手勢監聽,根據位置和距離來判斷當前選中的頭部,然后通知SectionList進行相對應的操作。
??首先我們生成Text,然后對其進行高度測量(便于之后的手勢控制使用)

 _getSections = () => {
        let array = new Array();
        for (let i = 0; i < this.props.sections.length; i++) {
            array.push(
                <View
                    style={styles.sectionView}
                    pointerEvents="none"
                    key={i}
                    ref={'sectionItem' + i}>
                    <Text
                        style={styles.sectionItem}>{this.props.sections[i]}</Text>
                </View>)
        }
        return array;
    }

 componentDidMount() {
        //它們的高度都是一樣的,所以這邊只需要測量一個就好了
        const sectionItem = this.refs.sectionItem0;

        this.measureTimer = setTimeout(() => {
            sectionItem.measure((x, y, width, height, pageX, pageY) => {
                this.measure = {
                    y: pageY,
                    height
                };
            })
        }, 0);
    }

由于它們每一項的高度是一樣的,所以這邊只需要測量一個的高度,其他的也就都知道了
??然后我們將這些Text展示到View中去,并對View進行手勢控制的監聽

 <View
       style={styles.container}
       ref="view"
       onStartShouldSetResponder={returnTrue}
       onMoveShouldSetResponder={returnTrue}
       onResponderGrant={this.detectAndScrollToSection}
       onResponderMove={this.detectAndScrollToSection}
       onResponderRelease={this.resetSection}>
       {this._getSections()}
 </View>
const returnTrue = () => true;

從代碼中我們可以看出,這邊我們需要處理的是手勢的Move和抬起事件,那么首先我們來看Move的操作。

detectAndScrollToSection = (e) => {
        var ev = e.nativeEvent.touches[0];
        // 手指按下的時候需要修改顏色
        this.refs.view.setNativeProps({
            style: {
                backgroundColor: 'rgba(0,0,0,0.3)'
            }
        })
        let targetY = ev.pageY;
        const {y, height} = this.measure;
        if (!y || targetY < y) {
            return;
        }
        let index = Math.floor((targetY - y) / height);
        index = Math.min(index, this.props.sections.length - 1);
        if (this.lastSelectedIndex !== index && index < this.props.sections.length) {
            this.lastSelectedIndex = index;
            this.onSectionSelect(this.props.sections[index], index, true);
            this.setState({text: this.props.sections[index], isShow: true});
        }
    }
 onSectionSelect(section, index, fromTouch) {
        this.props.onSectionSelect && this.props.onSectionSelect(section, index);

        if (!fromTouch) {
            this.lastSelectedIndex = null;
        }
    }

從代碼中我們可以知道,首先我們要對View的背景顏色進行改變,這樣可以讓我們知道已經選中了該View了,然后獲取我們當前觸摸點的坐標,在之前我們已經計算每個Text的高度,然后我們根據這些就可以計算出當前觸摸點之下的是哪個分組了。最后通過
this.onSectionSelect(this.props.sections[index], index, true); this.setState({text: this.props.sections[index], isShow: true});
分別進行外部列表的通知和當前View的通知
??然后我們來看手勢抬起時候的操作

 resetSection = () => {
        // 手指抬起來的時候需要變回去
        this.refs.view.setNativeProps({
            style: {
                backgroundColor: 'transparent'
            }
        })
        this.setState({isShow: false})
        this.lastSelectedIndex = null;
        this.props.onSectionUp && this.props.onSectionUp();
    }

從代碼之中我們可以知道,該方法主要是處理View背景的變化,以及抬起時候的一些通知。我們先看下整體的效果

2.jpeg

接下來就是選擇的一個提示了(類似于微信通訊錄中間的彈窗通知)。首先我們創建我們需要的一個視圖

 <View
       pointerEvents='box-none'
       style={styles.topView}>
        {this.state.isShow ?
         <View style={styles.modelView}>
             <View style={styles.viewShow}>
               <Text style={styles.textShow}>{this.state.text}</Text>
          </View>
       </View> : null
       }
      <View
           style={styles.container}
           ref="view"
           onStartShouldSetResponder={returnTrue}
           onMoveShouldSetResponder={returnTrue}
           onResponderGrant={this.detectAndScrollToSection}
           onResponderMove={this.detectAndScrollToSection}
           onResponderRelease={this.resetSection}>
               {this._getSections()}
       </View>
  </View>

在這里我們要注意的是,由于我們的展示視圖是在屏幕的中間位置,并且在Android上子視圖超出父視圖的部分無法顯示(也就是設置left:-100這樣的屬性會讓視圖部分無法看見)。所以這里我們使用的包裹展示視圖的父視圖是全屏的,那么這個pointerEvents='box-none'就尤其重要,它可以保證當前視圖不操作手勢控制,而子視圖可以操作。假如這邊不設置的話,會導致SectionList無法滾動,因為被當前視圖蓋住了。具體的屬性介紹可以查看RN官方文檔對View屬性的介紹
??在上面的方法之中我們有需改state的值,這邊就是來控制展示視圖的顯示隱藏的。我們來看下效果

3.jpeg

3.3 列表的分組跳轉

上面做到了列表的展示,接下來就是列表的分組跳轉了,在RN的介紹文檔上可以看到VirtualizedList(FlatList和SectionList都是對它的封裝)有如下的方法

scrollToEnd(params?: object) 
scrollToIndex(params: object) 
scrollToItem(params: object) 
scrollToOffset(params: object) 

從名稱上看就是對列表的滾動,然后找到FlatList,里面有更詳細的介紹,這邊我們使用的方法是scrollToIndex

scrollToIndex(params: object) 
Scrolls to the item at a the specified index such that it is positioned in the viewable area such that viewPosition
 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle.
如果不設置getItemLayout屬性的話,可能會比較卡。

那么從最后一句話我們知道在這里我們需要去設置getItemLayout屬性,這樣就可以告訴SectionList列表的高度以及每個項目的高度了

const ITEM_HEIGHT = 50; //item的高度
const HEADER_HEIGHT = 24;  //分組頭部的高度
const SEPARATOR_HEIGHT = 0;  //分割線的高度

_getItemLayout(data, index) {
   let [length, separator, header] = [ITEM_HEIGHT, SEPARATOR_HEIGHT, HEADER_HEIGHT];
   return {length, offset: (length + separator) * index + header, index};
 }

同時我們要注意的是在設置SectionList的renderItem和renderSectionHeader,也就是SectionList的分組內容和分組頭部的組件的高度必須是我們上面給定計算時候的高度(這點很重要)
??那么這個時候根據上面右側頭部展示組件給的回調的值(配合我們第一步得到的citySectionSize)就可以進行

對應的列表滾動了。

<View style={{paddingTop: Platform.OS === 'android' ? 0 : 20}}>
      <View>
        <SectionList
             ref='list'
             enableEmptySections
             renderItem={this._renderItem}
             renderSectionHeader={this._renderSectionHeader}
             sections={this.state.data}
             getItemLayout={this._getItemLayout}/>

        <CitySectionList
              sections={ this.state.sections}
              onSectionSelect={this._onSectionselect}/>
       </View>
</View>

 //這邊返回的是A,0這樣的數據
_onSectionselect = (section, index) => {
    //跳轉到某一項
   this.refs.list.scrollToIndex({animated: true, index: this.state.sectionSize[index]})
 }

但是假如僅僅只是這樣的話,你會發現在使用的時候會報錯,錯誤是找不到scrollToIndex方法。wtf??RN官方文檔上明明有這個方法啊。然而其實FlatList對VirtualizedList封裝的時候有添加這些方法,而SectionList并沒有。那么,只能自己動手添加了,參照
FlatList里面的scrollToIndex方法,為SectionList添加對于的方法。
??其中SectionList的路徑為
node_modules/react-native/Libraries/Lists/SectionList.js,代碼格式化后大概在187行的位置,修改如下

class SectionList<SectionT: SectionBase<any>>
    extends React.PureComponent<DefaultProps, Props<SectionT>, void> {
    props: Props<SectionT>;
    static defaultProps: DefaultProps = defaultProps;

    render() {
        const List = this.props.legacyImplementation ? MetroListView : VirtualizedSectionList;
        return <List
            ref={this._captureRef}
            {...this.props} />;
    }

    _captureRef = (ref) => {
        this._listRef = ref;
    };

    scrollToIndex = (params: { animated?: ?boolean, index: number, viewPosition?: number }) => {
        this._listRef.scrollToIndex(params);
    }
}

同時還需要修改VirtualizedSectionList的代碼,路徑在node_modules/react-native/Libraries/Lists/VirtualizedSectionList.js,大概253行處修改如下

  render() {
        return <VirtualizedList
            ref={this._captureRef}
            {...this.state.childProps} />;
    }

    _captureRef = (ref) => {
        this._listRef = ref;
    };

    scrollToIndex = (params: { animated?: ?boolean, index: number, viewPosition?: number }) => {
        this._listRef.scrollToIndex(params);
    }

修改完畢,我們來看下效果ios和android平臺下的效果如和

ios上的效果
android上的效果

??從上面的效果上可以看出來ios上的效果比android上的效果要好,然后在ios上有分組懸停的效果而在andorid并沒有,這是由于平臺特性決定的。在手動滾動的時候白屏的時間較短,而在跳轉的時候白屏的時間較長,但是相比與之前ListView時期的長列表的效果而言,要好太多了。也是期待RN以后的發展中對列表更好的改進吧。

4.最后

在完成這個城市選擇列表的時候主要參考了
http://reactnative.cn/docs/0.44/sectionlist.html
http://reactnative.cn/docs/0.44/flatlist.html
https://github.com/sunnylqm/react-native-alphabetlistview
最后附上項目地址:https://github.com/hzl123456/SectionListDemo
注意要記得修改SectionList和VirtualizedSectionList的代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容

  • 內容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,843評論 22 665
  • 各位領導的講話,各位代表老師的講話,各位同學的宣言。 每一句都深深印入腦海中,每一句話都戳到了心中,在百日誓...
    瑰之水閱讀 228評論 0 1
  • 懷揣著對南京的向往和遺憾,終于坐上了飛往南京的飛機?。激動,好久木有這種感覺,然而遺憾的是沒有小伙伴一起拍照。只能...
    Xanthe_c51f閱讀 165評論 0 0
  • 昨天有幸成為班級秘書長,心里有一點小激動!感覺責任重大了好多!感謝大家的支持,我會努力做好的,事事做到帶頭的作用,
    劉嘉禾爸爸閱讀 275評論 3 4