類似QQ好友列表的可伸縮分組列表璧函,相信對于一部分的開發(fā)者來說還是有需要用到的锨并,本文將會結(jié)合自己的開發(fā)理解和已發(fā)布在github和npm上的實例精刷,結(jié)合講解如何使用React Native快速封裝此類組件拒啰。你也可以查看已經(jīng)封裝并發(fā)布在Github上的組件react-native-expandable-section-list查看效果斑唬,當(dāng)然在你的項目中,你也可以直接使用該組件肌蜻,如發(fā)現(xiàn)任何bug請及時提出issue給我互墓,我會認真處理,如果你覺得組件做的還不錯蒋搜,請給我一個star哦篡撵!
封裝思路解析
原生ios開發(fā)階段時,UITableView功能強大豆挽,實現(xiàn)類似QQ列表這樣的功能育谬,你只需要在UITableView的delegate方法中作相關(guān)控制即可實現(xiàn),但是最開始轉(zhuǎn)到react native的時候帮哈,由于當(dāng)時剛參加工作膛檀,沒什么思路,后經(jīng)過一些嘗試和封裝,才漸漸有了現(xiàn)在的實現(xiàn)方法
思路一: 通過View的onLayout回調(diào)記錄分組布局大小宿刮,控制打開組高度和關(guān)閉組高度實現(xiàn)類似開關(guān)分組效果(這個思路和代碼是一個同事的方法,代碼稍微有點繞私蕾,但是可以控制打開關(guān)閉的動畫僵缺,我暫時放棄了使用,但是在寫下一篇文章會來講解這個方法的思路和實現(xiàn))
思路二: 控制數(shù)據(jù)踩叭,改變開關(guān)分組的標記值磕潮,并結(jié)合react native的state渲染實現(xiàn)類似開關(guān)分組效果(這是當(dāng)前用在實際項目中的一個組件,下面會具體介紹如何實現(xiàn)它的代碼和思路)
代碼解析
首先渲染一個全屏的ListView容贝,ListView中的row當(dāng)作分組自脯,row渲染成下列組件:
- 分組渲染:
_renderRow = (rowData, sectionId, rowId) => { // eslint-disable-line
const { renderRow, renderSectionHeaderX, renderSectionFooterX, headerKey, memberKey } = this.props;
let memberArr = rowData[memberKey];
if (!this.state.memberOpened.get(rowId) || !memberArr) {
memberArr = [];
}
return (
<View>
<TouchableOpacity onPress={() => this._onPress(rowId)}>
{ renderSectionHeaderX ? renderSectionHeaderX(rowData[headerKey], rowId) : null}
</TouchableOpacity>
<ScrollView scrollEnabled={false}>
{
memberArr.map((rowItem, index) => {
return (
<View key={index}>
{renderRow ? renderRow(rowItem, index, sectionId) : null}
</View>
);
})
}
{ memberArr.length > 0 && renderSectionFooterX ? renderSectionFooterX(rowData, sectionId) : null }
</ScrollView>
</View>
);
}
說明:如代碼,每個Row
中都由renderSectionHeaderX
斤富、renderRow
膏潮、renderSectionFooterX
組成,renderSectionHeaderX
通過Touchable
組件的點擊事件满力,可以控制下面的ScrollView
包著的一個個組成員renderRow
焕参,及sectionFooter
,每次渲染這樣一個分組的時候通過當(dāng)前組的開關(guān)狀態(tài)this.state.memberOpened
來獲得當(dāng)前memberArr
數(shù)組是空數(shù)組或是你的傳入數(shù)據(jù)rowData[memberKey]
,當(dāng)然也可以控制你的關(guān)閉狀態(tài)不一定是空數(shù)組油额,這里的開關(guān)狀態(tài)寄存器是一個Map
對象叠纷,或者你也可以自己構(gòu)造合適的鍵值對對象
- 開關(guān)對象構(gòu)造:
constructor(props) {
super(props);
this.ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
let map = new Map();
if (props.dataSource && props.isOpen) {
props.dataSource.map((item, i) => map.set(i.toString(), true))
}
if (props.openOptions) {
props.openOptions.map((item) => map.set(item.toString(), true))
}
this.state = {
memberOpened: map
}
}
說明:構(gòu)造狀態(tài)寄存器,通過屬性設(shè)置默認的分組開關(guān)狀態(tài)潦嘶,isOpen
是bool
值涩嚣,記錄是否全部打開分組, openOptions
是一個數(shù)組,里面記錄你選擇打開哪些分組掂僵,如打開0, 2分組航厚,即為[0,2]
- 點擊開關(guān)方法:
_onPress = (i) => {
this.setState((state) => {
const memberOpened = new Map(state.memberOpened);
memberOpened.set(i, !memberOpened.get(i)); // toggle
return { memberOpened };
});
if (this.props.headerOnPress) {
this.prop.headerOnPress(i, this.state.memberOpened.get(i) || false);
}
LayoutAnimation.easeInEaseOut();
};
說明:每次點擊組頭,執(zhí)行該方法锰蓬,改變當(dāng)前組在狀態(tài)寄存器中設(shè)置的狀態(tài)阶淘,最開始思路甚至把這狀態(tài)寄存設(shè)置成為每個分組數(shù)據(jù)的某個特定屬性,后來發(fā)現(xiàn)Map
拿來這里當(dāng)作一種寄存器用真的很完美互妓。
- 數(shù)據(jù)源
const MockData = [
...
{
header: 'sectionHeader',
member: [
...
{
title: 'memberTitle',
content: 'content',
},
...
]
},
...
]
特定組件是拿來做特定用途的溪窒,react-native-expandable-section-list只適用于本文說明的情況,當(dāng)然冯勉,也對數(shù)據(jù)源做一定的限制澈蚌,從而快速封裝出來QQ列表的可伸縮分組效果。
FlatList擴展封裝
react native在版本0.43后提出使用FlatList灼狰,在使用FlatList的時候宛瞄,通過同樣的思路,也對FlatList做了類似的擴展交胚,組件可見react-native-expandable-section-flatlist份汗,0.43版本后的FlatList確實是對列表組件性能做了極大的提升盈电,數(shù)據(jù)源data
屬性,加上擴展數(shù)據(jù)屬性extraData
的使用讓每次的state組件渲染不會再是渲染當(dāng)前列表杯活,而是直接定位到你作出改變的分組從而改變分組的開關(guān)狀態(tài)匆帚。keyExtractor
屬性也更加利于定位每一個分組的位置和設(shè)定。在數(shù)據(jù)測試時旁钧,暫只做了100個分組的測試吸重,F(xiàn)latList的性能原理是只會顯示你看到的這部分分組,做的還是相當(dāng)好的歪今,暫未發(fā)現(xiàn)性能影響嚎幸。我盡量減少了對原組件FlatList的屬性影響,其他類似上拉刷新寄猩,下拉加載等屬性也全部兼容嫉晶,接下來直接展示精簡后的代碼:
class ExpanableList extends Component {
constructor(props) {
super(props);
let map = new Map();
if (props.dataSource && props.isOpen) {
props.dataSource.map((item, i) => map.set(i, true))
}
if (props.openOptions) {
props.openOptions.map((item) => map.set(item, true))
}
this.state = {
memberOpened: map
}
}
static propTypes = {
dataSource: PropTypes.array.isRequired,
headerKey: PropTypes.string,
memberKey: PropTypes.string,
renderRow: PropTypes.func,
renderSectionHeaderX: PropTypes.func,
renderSectionFooterX: PropTypes.func,
headerOnPress: PropTypes.func,
isOpen: PropTypes.bool,
openOptions: PropTypes.array,
};
static defaultProps = {
headerKey: 'header',
memberKey: 'member',
isOpen: false,
};
_keyExtractor = (item, index) => index;
_onPress = (i) => {
this.setState((state) => {
const memberOpened = new Map(state.memberOpened);
memberOpened.set(i, !memberOpened.get(i)); // toggle
return { memberOpened };
});
if (this.props.headerOnPress) {
this.prop.headerOnPress(i, this.state.memberOpened.get(i) || false);
}
LayoutAnimation.easeInEaseOut();
};
_renderItem = ({ item, index }) => { // eslint-disable-line
const { renderRow, renderSectionHeaderX, renderSectionFooterX, headerKey, memberKey } = this.props;
const sectionId = index;
let memberArr = item[memberKey];
if (!this.state.memberOpened.get(sectionId) || !memberArr) {
memberArr = [];
}
return (
<View>
<TouchableOpacity onPress={() => this._onPress(sectionId)}>
{ renderSectionHeaderX ? renderSectionHeaderX(item[headerKey], sectionId) : null}
</TouchableOpacity>
<ScrollView scrollEnabled={false}>
{
memberArr.map((rowItem, rowId) => {
return (
<View key={rowId}>
{renderRow ? renderRow(rowItem, rowId, index) : null}
</View>
);
})
}
{ memberArr.length > 0 && renderSectionFooterX ? renderSectionFooterX(item, sectionId) : null }
</ScrollView>
</View>
);
};
render() {
const { dataSource } = this.props;
return (
<FlatList
{...this.props}
data={dataSource}
extraData={this.state}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
/>
);
}
}
寫在最后
react-native-expandable-section-list和react-native-expandable-section-flatlist封裝的相對簡單,但是還是很實用的田篇,一點小經(jīng)驗分享希望能對你有所幫助车遂。
寫總結(jié)和分享文章確實是一件快樂的事情,第一篇文章React Native路由理解和react-navigation庫封裝學(xué)習(xí)受到了一些人的肯定斯辰,真的很開心舶担,謝謝各位,一起進步1蛏搿衣陶!