今天我要实现一个 类似于 iOS 开发中带有分组的colllectionView 样式的布局, 每个section都要有个组头。
首先我们要先决定要使用什么控件。ScrollView和ListView/FlatList还有SectionList都是可以选择的。
- ScrollView 会把所有子元素一次性全部渲染出来。使用上最简单。但是如果你有一个特别长的列表需要显示,可能会需要好几屏的高度。这时就会占用很大的内存去创建和渲染那些屏幕以外的JS组件和原生视图,性能上也会有所拖累。
- ListView 更适用于长列表数据。它会惰性渲染子元素,并不会立即渲染所有元素,而是优先渲染屏幕上可见的元素。
- FlatList 是0.43版本开始新出的改进版的ListView,性能更优,但是官方说现在可能不够稳定,尚待时间考验。但是它不能够分组/类/区(section)。
- SectionList 也是0.43版本推出的, 高性能的分组列表组件。但是它不支持头部吸顶悬浮的效果,但是也不要伤心,官方在下一个版本开始就可以支持悬浮的section头部啦 😜。
好啦, 综上所诉我选择使用SectionList ,现在开始干活吧 ✌️
首先
第一步我们先把要显示的样式写好作为子控件, 把数据源整理好。
例一、
<SectionList
renderItem={({item}) => <ListItem title={item.title} />}
renderSectionHeader={({section}) => <H1 title={section.key} />}
sections={[ // 不同section渲染相同类型的子组件
{data: [...], key: ...},
{data: [...], key: ...},
{data: [...], key: ...},
]}
/>
例二、
<SectionList
sections={[ // 不同section渲染不同类型的子组件
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
]}
/>
sections 就是我们的数据源,每一个data 就是我们要用的item, renderItem就是你要显示的子控件哦。如果你每个组都复用一个子组件那就按照例一的结构, 如果你想要不同的组返回不同样式的子组件那就按照例二的结构返回不同的renderItem即可。
这里提个醒, key一定要有, 不同的section 要设置不同的key才会渲染相应的section, 如果你key值都相同, 那可能会出现只显示一组数据的情况哦~
下面来看看我的代码:
<SectionList
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
sections={[ // 不同section渲染相同类型的子组件
{ data: [{ title: this.state.appModel[0] }], key: this.state.groupsModel[0].title },
{ data: [{ title: this.state.appModel[1] }], key: this.state.groupsModel[1].title },
{ data: [{ title: this.state.appModel[2] }], key: this.state.groupsModel[2].title },
{ data: [{ title: this.state.appModel[3] }], key: this.state.groupsModel[3].title },
]}
/>
这样有了最基础的样式, 四组纵向的列表, 但是我要横向的, 于是我要设置他的样式啦。
接下来
这里我添加两个属性:
contentContainerStyle={styles.list}//设置cell的样式
pageSize={4} // 配置pageSize确认网格数量
const styles = StyleSheet.create({
list: {
//justifyContent: 'space-around',
flexDirection: 'row',//设置横向布局
flexWrap: 'wrap', //设置换行显示
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
});
好啦, 让我们来看看效果。
😓这是什么鬼???
为什么它的组头也在左边 , 并且他的其他组数据都横着了, 对于小白我来说只有大写的懵~。不知道你们有没有遇到这种情况, 是什么原因导致的, 我很是困惑啊, 当我把
renderSectionHeader={this._renderSectionHeader}
这行代码注掉的时候, 它的显示是正常的...
这就尴尬了...
它的每一个小方块是一个item,达不到我要的效果啊, 于是我决定换个思路, 谁让我是打不死的小白呢😝
重新来
我决定让每个section是一个item。在每个item上创建多个可点击的板块。
_renderItem = ({ item}) => (
<View style={styles.list}>
{
item.map((item, i) => this.renderExpenseItem(item, i))
}
</View>
);
renderExpenseItem(item, i) {
return <TouchableOpacity key={i} onPress={() => this._pressRow(item)} underlayColor="transparent">
<View style={styles.row}>
<CellView source={item.img}></CellView>
</View>
</TouchableOpacity>;
}
_renderSectionHeader = ({ section }) => (
<View style={{ flex: 1, height: 25 }}>
<Text style={styles.sectionHeader} >{section.key}</Text>
</View>
);
render() {
return (
<View style={{ flex: 1 }}>
<Text style={styles.navigatorStyle}> 发现 </Text>
<View style={{ flex: 1, backgroundColor: '#F7F6F8' }}>
<SectionList
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
showsVerticalScrollIndicator={false}
sections={ // 不同section渲染相同类型的子组件
this.state.dataSource
}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
list: {
//justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
row: {
backgroundColor: '#FFFFFF',
justifyContent: 'center',
width: (ScreenWidth - 1) / 4,
height: (ScreenWidth - 1) / 4,
alignItems: 'center',
},
sectionHeader: {
marginLeft: 10,
padding: 6.5,
fontSize: 12,
color: '#787878'
},
});
这里的dataSource 我是在之前数据的基础上又包了一层[],然后在renderItem里做了map映射, 这样每个renderItem上返回了每一组我所需要的子组件。快来看看我的变化吧😊
肿么样, 达到效果了吧, 但是你有没有发现 底部为啥是黄色的?,我可没有去设置这么丑的颜色哦,其实它是提醒我们有不完美的地方, 下面就让我们解决一下这个不完美吧 。
最后解决问题
最后让我们来解决问题。它警告我们每个item 要有不同的key ,还记不记得我上面的提醒,我也犯这个错误了。
- 默认情况下每行都需要提供一个不重复的key属性。你也可以提供一个keyExtractor函数来生成key。
// 把这个属性添加到 <SectionList/> 里面
keyExtractor = {this._extraUniqueKey}
_extraUniqueKey(item ,index){
return "index"+index+item;
}
这是每个item要设置key, 同样每个子控件也不能放过, 一定要设置它的key, 要不然这个屎黄色一直伴着你 多烦~~~
最后看一下我最终的代码吧!
var Dimensions = require('Dimensions');//获取屏幕的宽高
var ScreenWidth = Dimensions.get('window').width;
var ScreenHeight = Dimensions.get('window').height;
// const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);// 这个是创建动画
export default class Explore extends Component {
constructor(props) {
super(props);
this.state = {
appModel: null,
groupsModel: null,
dataSource: null,
}
}
//Component挂载完毕后调用
componentDidMount() {
this.fetchData();
}
async fetchData() {
try {
let model = await NetFetch.post(_req_url_path, {
});
let apps = model.apps;
let groups = model.groups;
let data = [];
for (let i = 0; i < model.groups.length; i++) {
let row = [];
for (let j = 0; j < model.apps.length; j++) {
if (model.groups[i].appIds.indexOf(model.apps[j].appId) >= 0) {
row.push(model.apps[j]);
}
}
data.push({ data: [row], key: model.groups[i].title });
}
// 这里我重组了一下数据结构, 看没看见我在row外面又包了一层, 为了我循环创建每个section的子组件。
this.setState({
appModel: model.apps,
groupsModel: model.groups,
dataSource: data
});
} catch (error) {
alert(error.msg);
}
}
_renderItem = ({ item}) => (
<View style={styles.list}>
{
item.map((item, i) => this.renderExpenseItem(item, i))
}
</View>
);
renderExpenseItem(item, i) {
return <TouchableOpacity key={i} onPress={() => this._pressRow(item)} underlayColor="transparent">
<View style={styles.row}>
<CellView source={item.img}></CellView>
</View>
</TouchableOpacity>;
}
_renderSectionHeader = ({ section }) => (
<View style={{ flex: 1, height: 25 }}>
<Text style={styles.sectionHeader} >{section.key}</Text>
</View>
);
_listHeaderComponent() {
return (
<HeaderView integral={0}></HeaderView>
);
}
_listFooterComponent() {
return (
<Text style={[styles.remark]}>*预期收益非平台承诺收益,市场有风险,投资需谨慎</Text>
);
}
_pressRow(item) {
this.props.navigator.pushTo(item.go)
}
_extraUniqueKey(item ,index){
return "index"+index+item;
}
render() {
if (!this.state.dataSource) {
return (
<View></View>
);
}
return (
<View style={{ flex: 1 }}>
<Text style={styles.navigatorStyle}> 发现 </Text>
<View style={{ flex: 1, backgroundColor: '#F7F6F8' }}>
<SectionList
contentInset={{top:0,left:0,bottom:49,right:0}}// 设置他的滑动范围
renderItem={this._renderItem}
ListFooterComponent={this._listFooterComponent}
ListHeaderComponent={this._listHeaderComponent}
renderSectionHeader={this._renderSectionHeader}
showsVerticalScrollIndicator={false}
keyExtractor = {this._extraUniqueKey}// 每个item的key
// contentContainerStyle={styles.list}
// horizontal={true}
// pageSize={4} // 配置pageSize确认网格数量
sections={ // 不同section渲染相同类型的子组件
this.state.dataSource
}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
navigatorStyle: {
height: 64,
backgroundColor: '#FFFFFF',
textAlign: 'center',
paddingTop: 33.5,
fontSize: 17,
fontWeight: '600',
},
list: {
//justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
row: {
backgroundColor: '#FFFFFF',
justifyContent: 'center',
width: (ScreenWidth - 1) / 4,
height: (ScreenWidth - 1) / 4,
alignItems: 'center',
// borderWidth: 0.5,
// borderRadius: 5,
// borderColor: '#E6E6E6'
},
sectionHeader: {
marginLeft: 10,
padding: 6.5,
fontSize: 12,
color: '#787878'
},
remark: {
margin: 10,
fontSize: 10,
color: '#D2D2D2',
marginBottom: 10,
alignSelf: 'center',
},
});
看下最终效果图吧
最最后总结一下在开发中遇到的疑难杂症还有sectionList的重要属性。
不知道你有没有遇见这个问题, 看起来很简单, 应该是我没有引入Text组件, 但是我确实引入了。最终发现这个问题竟是因为我有段代码是这样写的
<Image> source={require('../../assets/image/deadline.png')} style={styles.iconStyle} </Image>
<Text style={styles.userNameStyle}>赚积分,换好礼!</Text>
不知道你有没有发现错误, 由于习惯我<Image>组件写成<Image></Image>,Image是自封闭标签所以
<Image source={require('../../assets/image/deadline.png')} style={styles.iconStyle} />
<Text style={styles.userNameStyle}>赚积分,换好礼!</Text>
这样问题就解决了, 但是我不清楚它为啥会报这样的错,反正开发中总是会出现一些不知所以的错, 所以平时写代码的时候不断总结起来就好啦...
SectionList 属性
- ItemSeparatorComponent?: ?ReactClass<any>
行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后。
- ListFooterComponent?: ?ReactClass<any>
尾部组件
- ListHeaderComponent?: ?ReactClass<any>
头部组件
- keyExtractor: (item: Item, index: number) => string
此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标。
- onEndReached?: ?(info: {distanceFromEnd: number}) => void
当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold个像素的距离时调用。
- onRefresh?: ?() => void
如果设置了此选项,则会在列表头部添加一个标准的RefreshControl控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing属性。
- refreshing?: ?boolean
是否刷新喽
- renderItem: (info: {item: Item, index: number}) => ?React.Element<any>
根据行数据data渲染每一行的组件。
除data外还有第二个参数index可供使用。
- renderSectionHeader?: ?(info: {section: SectionT}) => ?React.Element<any>
这就是我用到的每个section的组头
- sections: Array<SectionT>
你的数据源
最后再提醒一下不要忘了key key key 哦 。