React Native——ListView的使用详解

6,212 阅读5分钟

ListView是什么

ListView - 一个核心组件,用于高效地显示一个可以垂直滚动的变化的数据列表。最基本的使用方式就是创建一个ListView.DataSource数据源,然后给它传递一个普通的数据数组,再使用数据源来实例化一个ListView组件,并且定义它的renderRow回调函数,这个函数会接受数组中的每个数据作为参数,返回一个可渲染的组件(作为listview的每一行)

简单说它就是一个列表组件,用来显示列表视图,类似Android中的ListView,iOS中的UITableView。作为列表组件,ListView是非常常用的。虽然官方文档中指出ListView已经过期,指定FlatList和SectionList替代ListView来使用,但我认为还是有必要对这个控件加深理解,在很多情况下使用ListView还是很方便的。

ListView的相关属性

这里我只列出平常会用的比较多的重要属性加以讲解,其它属性可以查阅官方文档。

dataSource——ListView.DataSource实例,用来设置列表数据源

renderRow——方法,用来渲染列表中每一行,该方法有四个参数(rowData, sectionID, rowID, highlightRow),可以通过传递参数来设置每行的数据,区分不同的section和row。四个参数分别表示数据源中一条数据,分组的ID,行的ID,以及标记是否是高亮选中的状态信息。

有了dataSource和renderRow这两个属性,我们就可以完成一个ListView视图了,这两个属性是ListView最基本的属性,是必须要设置的。

renderHeader——渲染头部视图,以iOS为例,UITableView有一个tableHeaderView属性,可以设置头部视图,这里renderHeader也可以渲染一个自定义的头部视图。

renderFooter——渲染底部视图,类似于UITableView组件的tableFooterView,可以设置一个尾部视图。

renderSectionHeader——为每个section渲染一个粘性的头部视图,类似于UITableView中的sectionHeader视图。

stickySectionHeadersEnabled——sectionHeader是否支持粘性,实际就是是否支持ListView在滑动时当前sectionHeader悬停在上方。这个效果跟UITableView中plain样式的sectionHeader效果一样。

renderSeparator——渲染每行的分割线,通常我不习惯这么做,可以直接在renderRow中给行视图底部设置一个宽度,使用borderBottomWidth和borderColor来实现分割线。

onEndReached——当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold个像素的距离时调用。这个方法就是我们用来做列表的上拉加载时用到的。

onEndReachedThreshold——这个属性在ListView中是数值,当列表被滚动到底部不足这个值的距离时会触发onEndReached中的方法。在FlatList和SectionList中这个值是比值0~1之间,这里需要区分清楚。

ListView的用法

本篇文章我们使用ListView实现三种不同的列表视图,基本可以涵盖我们日常使用ListView的情况。

第一种,简单列表视图的实现

如图,要实现这样一个列表,我们只需要设置数据源dataSource和renderRow就可以了。

在使用dataSource时,我们需要先new一个dataSource对象,对于单个section的列表,使用rowHasChanged(prevRowData, nextRowData)和cloneWithRows(dataBlob, rowIdentities)来构造数据源。

对于每行视图,我们需要一个image组件来显示图片,一个Text组件显示文本,使用TouchableOpacity组件将图片和文本包裹起来,并给它设置点击事件,renderRow就完成了。

代码如下:

constructor(props) {  
    super(props);  
    let ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2  });  
    this.state = {    
        dataSource: ds.cloneWithRows(data)  
    }
}

render() {  
    return (    
        <View style={styles.container}>      
            <ListView        
                dataSource={this.state.dataSource}        
                renderRow={this._renderRow}      
            />    
        </View>  
    )
}


_renderRow = (rowData) => {  
    return (    
        <TouchableOpacity style={styles.cellContainer} onPress={() => {}}>      
            <Image source={rowData.image} style={styles.image}/>      
            <Text style={styles.title}>{rowData.title}</Text>    
        </TouchableOpacity>  
    )
}

其中data是一个包含图片和title的数组。通过设置数据源和渲染行视图的方式我们就得到了一个简单的列表。

第二种,网格形式的列表

要实现这样一个Grid Layout的视图,其实很简单,只需要修改下上面的代码。将ListView内容的样式改为横向布局(flexDireaction:'row'),然后支持换行(flexWrap:'wrap'),renderRow中渲染的元素就会横向排列,如果所有元素一行显示不了就自动换行。

render() {  
    return (    
        <View style={{ flex: 1}}>      
            <ListView        
                contentContainerStyle={styles.listView}        
                dataSource={this.state.dataSource}        
                renderRow={this._renderRow}      
            />    
        </View>  
    )
}

ListView是继承于ScrollView的,所以这里我们可以给它设置内容样式,注意是contentContainerStyle而不是style。

listView: {  
    flexDirection:'row',  
    flexWrap:'wrap',  
    justifyContent:'space-between',  
    paddingLeft: 20,  
    paddingRight: 20,
},

然后调整renderRow中图片、文字和容器的样式,一个GridLayout的列表就完成了。

第三种,分组视图,包含单列和网格样式的列表

假设我们有这样一个列表,有好几个分组(section),每个section中的row显示样式不同,有的是单列的,有的是网格形式的。这时候我们就不能简单的像GridLayout那样将ListView的内容样式改变,因为ListView的内容样式改变后里面的所有元素都受到影响,要么全部纵向显示,要么全部横向显示,达不到我们想要的效果。这里有两种方法:

  1. 对于网格元素的section,此section数据源我们给它设置为只有一条数据的数组,在renderRow中使用一个View将所有子元素包裹起来,使用数组的map方法循环显示网格中的子元素。View的内容样式同GridLayout一样横向排列显示然后换行。分组的ListView数据源格式为
    { sectionID_1: [ rowData1, rowData2, ... ],  sectionID_2: [rowData], sectionID_3: [ rowData1, rowData2, ... ], ...}

    或者

    [[ rowData1, rowData2, ... ], [ rowData], [ rowData1, rowData2, ... ], ...]
    其中sectionID_2的分组就是要用来显示网格视图的。
  2. 在要显示网格元素的section中再嵌入一个ListView,方法同GridLayout一样。

以上两种方式其实原理一致,只不过使用控件不同。这部分的实现相对来说稍微复杂点,在Demo中我分了两种情况实现,具体逻辑请看demo。

这里需要说明的是,对于多个section的ListView和单一section的ListView,dataSource的实现是不一样的。

  • 单一section
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {  dataSource: ds.cloneWithRows(data)}
  • 多个section
let ds = new ListView.DataSource({  
    rowHasChanged: (r1, r2) => r1 !== r2,  
    sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
this.state = {  dataSource: ds.cloneWithRowsAndSections(data)}

效果图如下

Demo地址: github.com/mrarronz/re…