今天想跟你們分享一個用RN實現的組件 - ExpandableList。恩,沒什麼特殊的緣由,只是由於最近有一個需求要用到這東西,並且RN沒有提供現成的組件,因此很(不)開(得)心(已)地作了一個。下面兩張圖是用這個組件實現的兩個demo,github地址在這兒,有興趣的能夠戳https://github.com/SmallStoneSK/react-native-expandable-list瞅一眼,喜歡的還能夠star一個~javascript
若是有哪說的不對的,歡迎指出哦~java
好了,廢話很少說,直接進入正題。首先,咱們先肯定下要解決的問題:react
咱們先將ExpandableList這個組件拆解一下,看看都有哪些部分。看下面的這張圖,咱們能夠把一個ExpandableList當作是由一個個Group組成的,而每一個Group又了包含GroupHeader和GroupBody,而其實GroupBody自己又是一個List。git
分析完結構以後,思路瞬間就有了,這個結構用兩個循環就能夠表示出來了,就像下面這樣:github
<View> {data.map((groupItem, groupIndex) => { return ( <View key={`group-${groupIndex}`}> {renderGroupHeader.bind(this, groupItem. groupHeaderData, groupIndex)} {groupItem.groupListData.map((listItemData, listItemIndex) => { return ( <View key={`group-${groupIndex}-list-item-${listItemIndex}`}> {renderListItem.bind(this, listItemData, groupIndex, listItemIndex)} </View> ); })} </View> ); })} </View>
沒錯,結構是很輕易地表示出來了。可是問題來了,展開收起的這個動畫過程應該怎麼現實呢?咱們都知道在RN中若是要實現動畫,那Animated絕對是把好手。藉助Animated,咱們能夠很精準地控制動畫的實現,固然也包括這裏的展開/收起動畫。可是在這裏,就不勞煩這尊大佛啦~由於藉助LayoutAnimation,咱們能夠實現地更優雅(其實就是偷懶)。web
在講LayoutAnimation以前,不妨先回顧下web中的transition。爲啥捏?由於我的以爲這二者就是很像,只要給定了初始狀態和終止狀態,那這中間的動畫切換過程就不須要咱們關心了。再來看這個展開/收起的動畫,是否是很符合這個條件。每一個group都有兩種狀態,即open和closed。所以,當closed時,咱們設置groupBody的height爲0就能夠了。spring
爲何要考慮API的設計呢?由於這個組件實在太簡單,感受都編不下去了,不找個主題怎麼湊字數。。。固然,這是玩笑話。實際上,在封裝這個組件的時候,仍是遇到了一些調用上的問題,就好比:react-native
這些問題在接下來的代碼中都會有答案,因此請繼續往下看吧。數組
咱們能夠先敲定一下基礎的暴露出來的接口方法:函數
屬性 | 值類型 | 解釋 |
---|---|---|
data | Array | ExpandableList的中的數據,數組中每一個對象由groupHeaderData和groupListData構成 |
style | object | 做用在ExpandableList上的樣式 |
groupStyle | object | 做用在每一個group上的樣式 |
groupSpacing | number | group之間的間隙 |
implementedBy | string | 組件實現方式,一共有'View', 'ListView', 'FlatList'三種方式可選,默認值'FlatList' |
renderGroupHeader | function | 渲染GroupHeader的方法 |
renderGroupListItem | function | 渲染GroupListItem的方法 |
因此,咱們能夠這麼調用
<ExpandableList data={xxx} style={xxx} groupStyle={xxx} groupSpacing={xxx} implementedBy={xxx} renderGroupHeader={xxx} renderGroupListItem={xxx} />
import React, {Component} from 'react'; import { View, ListView, ScrollView, FlatList, LayoutAnimation } from 'react-native'; export class ExpandableList extends Component { constructor(props) { super(props); this._supportFlatList = this. _supportFlatList.bind(this); this._renderUsingView = this._renderUsingView.bind(this); this._renderUsingFlatList = this._renderUsingFlatList.bind(this); this._renderUsingListView = this._renderUsingListView.bind(this); } _supportFlatList() { return !!FlatList; } _renderUsingFlatList() { // ... } _renderUsingView() { // ... } _renderUsingListView() { // ... } render() { const strategy = { 'View': this._renderUsingView, 'ListView': this._renderUsingListView, 'FlatList': this._supportFlatList() ? this._renderUsingFlatList : this._renderUsingListView }; let {implementedBy} = this.props; if(!strategy[implementedBy]) { implementedBy = 'FlatList'; } return strategy[implementedBy](); } }
根據上面代碼中的render方法能夠看到,最終使用哪一種方式渲染咱們的ExpandableList,徹底取決於implementedBy是什麼,也就是把這個決定權交給調用的人。當implementedBy的值沒有設置,或者是一個不合法的值的時候,咱們默認就使用FlatList來實現。並且,還對FlatList進行了降級處理,若是不支持FlatList的話,就用ListView代替實現。
由於每個group都有自身的open/closed狀態,因此倒不如在state中維護一個狀態數組。並且啊,考慮到假若有這麼一個場景:列表在剛渲染出來的時候,有幾個group是open的,有幾個group是closed的。因此,咱們能夠這麼設計:
export class ExpandableList extends Component { constructor(props) { super(props); this.state = { groupStatus: this._getInitialGroupStatus() }; } _getInitialGroupStatus() { const {initialOpenGroups = [], data = []} = this.props; // true表明open, false表明closed return new Array(data.length) .fill(false) .map((item, index) => { return initialOpenGroups.indexOf(index) !== -1; }); } }
由於無論用哪一種方式去渲染,每一個group的結構是相同的,因此倒不如封裝一個_renderGroupItem方法,讓這3種不一樣的render方法調用。也就是這樣:
export class ExpandableList extends Component { toggleOpenStatus(index, closeOthers) { // 支持在切換自身狀態的時候,同時把其餘的group都關閉 const newGroupStatus = this.state.groupStatus.map((status, idx) => { return idx !== index ? (closeOthers ? false : status) : !status; }); this.setState({ groupStatus: newGroupStatus }); } _renderGroupItem(groupItem, groupId) { const status = this.state.groupStatus[groupId]; const {groupHeaderData = [], groupListData = []} = groupItem; const {renderGroupHeader, renderGroupListItem, groupStyle, groupSpacing} = this.props; const groupHeader = renderGroupHeader && renderGroupHeader({ status, groupId, item: groupHeaderData, toggleStatus: this.toggleGroupStatus.bind(this, groupId)} ); const groupBody = groupListData.length > 0 && ( <ScrollView bounces={false} style={!status && {height: 0}}> {groupListData.map((listItem, index) => ( <View key={`gid:${groupId}-rid:${index}`}> {renderGroupListItem && renderGroupListItem({ item: listItem, rowId: index, groupId })} </View> ))} </ScrollView> ); return ( <View key={`group-${groupId}`} style={[groupStyle, groupId && groupSpacing && {marginTop: groupSpacing}]} > {groupHeader} {groupBody} </View> ); } _renderFlatListItem({item, index}) { return this._renderGroupItem(item, index); } _renderListViewItem(rowData, groupId, rowId) { return this._renderGroupItem(rowData, parseInt(rowId)); } _renderUsingFlatList() { const {data=[], style} = this.props; return ( <FlatList data={data} style={style} showsVerticalScrollIndicator={false} keyExtractor={(item, index) => index} renderItem={this._renderFlatListItem} /> ); } _renderUsingView() { const {data = [], style} = this.props; return ( <View style={style}> {data.map((item, groupId) => { return this._renderGroupItem(item, groupId); })} </View> ); } _renderUsingListView() { const {data = [], style} = this.props; return ( <ListView style={style} showsVerticalScrollIndicator={false} renderRow={this._renderListViewItem} dataSource={new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }).cloneWithRows(data)} /> ); } }
稍微分析下上面的代碼:
_renderGroupItem分兩個部分渲染:header和body。可是須要注意的是,在執行renderGroupHeader方法的時候,注意其中的參數。還記得文章一開始討論的幾個問題嗎?status, groupId, item, toggleStatus這四個參數就能解決以前的疑惑了。
前面就提到過,用LayoutAnimation來實現咱們的動畫將很是簡單。因爲在以前的代碼中,咱們已經經過status來控制整個groupBody的height,因此咱們只要這樣就能夠:
export class ExpandableList extends Component { componentWillUpdate() { LayoutAnimation.easeInEaseOut(); // 也能夠用LayoutAnimation.spring() } }
是的,就只須要這一行代碼,列表在展開/收起的時候就不會幹巴巴的了。LayoutAnimation會自動計算height,並提供一個流暢的動畫。
說實話,其實代碼很簡單,只是用現成的組件進行一個封裝,可是要把方方面面的東西都考慮全了,還真是不容易。因此上面的代碼確定還有能夠優化的地方,以及擴展更多的功能。
最後仍是照慣例再貼個github的地址吧:https://github.com/SmallStoneSK/react-native-expandable-list