类似QQ好友列表的可伸缩分组列表,相信对于一部分的开发者来说还是有需要用到的,本文将会结合自己的开发理解和已发布在github和npm上的实例,结合讲解如何使用React Native快速封装此类组件。你也可以查看已经封装并发布在Github上的组件react-native-expandable-section-list查看效果,当然在你的项目中,你也可以直接使用该组件,如发现任何bug请及时提出issue给我,我会认真处理,如果你觉得组件做的还不错,请给我一个star哦!
封装思路解析
原生ios开发阶段时,UITableView功能强大,实现类似QQ列表这样的功能,你只需要在UITableView的delegate方法中作相关控制即可实现,但是最开始转到react native的时候,由于当时刚参加工作,没什么思路,后经过一些尝试和封装,才渐渐有了现在的实现方法
思路一: 通过View的onLayout回调记录分组布局大小,控制打开组高度和关闭组高度实现类似开关分组效果(这个思路和代码是一个同事的方法,代码稍微有点绕,但是可以控制打开关闭的动画,我暂时放弃了使用,但是在写下一篇文章会来讲解这个方法的思路和实现)
思路二: 控制数据,改变开关分组的标记值,并结合react native的state渲染实现类似开关分组效果(这是当前用在实际项目中的一个组件,下面会具体介绍如何实现它的代码和思路)
代码解析
首先渲染一个全屏的ListView,ListView中的row当作分组,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
,每次渲染这样一个分组的时候通过当前组的开关状态this.state.memberOpened
来获得当前memberArr
数组是空数组或是你的传入数据rowData[memberKey]
,当然也可以控制你的关闭状态不一定是空数组,这里的开关状态寄存器是一个Map
对象,或者你也可以自己构造合适的键值对对象
- 开关对象构造:
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
}
}
说明:构造状态寄存器,通过属性设置默认的分组开关状态,isOpen
是bool
值,记录是否全部打开分组, openOptions
是一个数组,里面记录你选择打开哪些分组,如打开0, 2分组,即为[0,2]
- 点击开关方法:
_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();
};
说明:每次点击组头,执行该方法,改变当前组在状态寄存器中设置的状态,最开始思路甚至把这状态寄存设置成为每个分组数据的某个特定属性,后来发现Map
拿来这里当作一种寄存器用真的很完美。
- 数据源
const MockData = [
...
{
header: 'sectionHeader',
member: [
...
{
title: 'memberTitle',
content: 'content',
},
...
]
},
...
]
特定组件是拿来做特定用途的,react-native-expandable-section-list只适用于本文说明的情况,当然,也对数据源做一定的限制,从而快速封装出来QQ列表的可伸缩分组效果。
FlatList扩展封装
react native在版本0.43后提出使用FlatList,在使用FlatList的时候,通过同样的思路,也对FlatList做了类似的扩展,组件可见react-native-expandable-section-flatlist,0.43版本后的FlatList确实是对列表组件性能做了极大的提升,数据源data
属性,加上扩展数据属性extraData
的使用让每次的state组件渲染不会再是渲染当前列表,而是直接定位到你作出改变的分组从而改变分组的开关状态。keyExtractor
属性也更加利于定位每一个分组的位置和设定。在数据测试时,暂只做了100个分组的测试,FlatList的性能原理是只会显示你看到的这部分分组,做的还是相当好的,暂未发现性能影响。我尽量减少了对原组件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封装的相对简单,但是还是很实用的,一点小经验分享希望能对你有所帮助。
写总结和分享文章确实是一件快乐的事情,第一篇文章React Native路由理解和react-navigation库封装学习受到了一些人的肯定,真的很开心,谢谢各位,一起进步!!