浅析Listview组件03之一堆属性

前言:【我不生产代码,我只是官方案例的搬运工,当然还是改了点内容的,假装它是原创的吧】刷新数据搞定之后,现在需要研究ListView的那一堆属性了,比如有和样式有关的头部、底部、段(section)标题之类,还有一些事件回调方法等。

属性之样式

先贴demo链接:demo v5地址
运行出来的界面是这样的(界面有点丑,海涵):

这里是一次性把头部底部段标题都给搞定,所以相对来说内容要稍微多一点,但都比较容易理解。

提供数据及附属对象

首先我们需要可供显示的数据(data),这个数据的构成就比之前那个最简demo要复杂不少。因为它需要包含两类数据,一个是段标题(SectionHeader),一个是每段的实际数据集(rowDatas),数据集包含rowData和可能会有的rowID。ListView并没有定死我们这个data的格式是怎样的,但也有一定的规律在里面,我们等会还需要自己写方法来指定怎么取段标题以及具体数据。我现在提供的格式是这样的:

1
2
3
4
5
var data = {
Sone: [1001, 1002, 1003, 1004],
Stwo: [2001, 2002, 2003, 2004],
Sthree: {up:3001,down:3002,left:3003, right:3004}
};

这个data是一个对象,段标题就是对象的key(即Sone、Stwo),每个段的数据集是这个key的值,类型是数组(也可以是对象,比如第三段,其中rowID就是up、down这些key,rowData是对应的值3001、3002等),可以对照上面的界面看看。
然后我们还需要提供两个附属的数组,即段标题数组,和段内容的相关key(rowID)的数组,它们长这样:

1
2
var sectionIDs = ['Sone','Stwo','Sthree'];
var rowIDs = [[0,1,2,3],[3,2,1,0],['up','down','left','right']]

注意这个一定要和data的结构对应清楚,否则一会取不到值。rowIDs这个二维数组里面的每个数组都对应着每段数据集的key(rowID),如果数据集是数组,那么其rowID就是数组的下标,如果是对象,那就是对象的key。

新建dataSource对象

这次我们新建dataSource的方法也多点内容:

1
2
3
4
5
6
var ds = new ListView.DataSource({
getRowData: this.getRowData,
getSectionHeaderData: this.getSectionData,
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});

那两个HasChanged方法就不用多说了,大家都知道干嘛的。我们来说说前面的两个方法getRowData(获取行数据),getSectionHeaderData(获取段标题),这个需要根据我们自己提供的数据构成来定制相应的方法。我这里方法是这么写的:

1
2
3
getRowData(dataBlob, sectionID, rowID){
return dataBlob[sectionID][rowID];
}

在这个获取行数据的方法中,dataBlob参数就是我们的data数据,sectionID就是我们刚提供的sectionIDs数组里面的值,rowID是我们提供的rowIDs的值。也就是说,我们返回的 dataBlob[sectionID][rowID]就是实际的行数据(rowData),比如data[‘Sone’][0]或者data[‘Sthree’][‘left’]

1
2
3
getSectionData(dataBlob, sectionID ){
return sectionID;
}

这个更简单,就是返回我们的段标题,这是从sectionIDs数组里面捞出来的。

赋值给state

这个赋值的方法也多了点东西:

1
2
3
this.state = {
dataSource: ds.cloneWithRowsAndSections(data, sectionIDs, rowIDs)
};

clone方法换成了参数更多的cloneWithRowsAndSections,它的参数就是我们之前提供的数据以及相关两个附属的数组。

ListView调用

这个组件调用是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ListView
dataSource={this.state.dataSource}
style={styles.listview}
onChangeVisibleRows={(visibleRows, changedRows) => console.log({visibleRows, changedRows})}
renderHeader={this.renderHeader}
renderFooter={this.renderFooter}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}

initialListSize={10}
pageSize={10}
scrollRenderAheadDistance={2000}
/>

先重点关注那几个render相关的方法,renderHeader和renderFooter只需要提供两个返回组件界面的的方法就行,届时它们会渲染成相应顶栏和底栏(貌似没什么实际用途……)。
renderSectionHeader有两个参数,sectionData和sectionID,前者是先前我们写的那个getSectionData方法返回的内容,后者是我们提供的sectionIDs 数组的内容,我这个demo里面两个值是同一个东西。这个方法需要返回一个组件界面。
renderRow可以用三个参数,rowData, sectionID, rowID。分别是行数据,以及sectionIDs的对应内容和rowIDs数组的对应内容。他也需要返回一个组件界面。

然后我们就可以运行demo看效果了。

##利用默认实现简化代码
我估计大家都觉得这个demo理解起来不难,但那个数据的提供有点烦人,data这个是必须提供的,没法简化。但sectionIDs数组和rowIDs的二维数组是个什么鬼,数据多了手写会爆掉的吧。
其实如果我们不提供sectionIDs和rowIDs,cloneWithRowsAndSections这个方法的源代码里面是有默认实现的,默认方法返回的数据,和我们刚才手动提供的数据其实是一样的(当然,那个rowIDs里面的[3,2,1,0]会按[0,1,2,3]的顺序排列)。你们可以把demo的
cloneWithRowsAndSections方法中的后两个参数给去掉,不会影响任何效果。
另外,getRowData的相关代码也可以去掉,因为它也有默认实现,和demo里面提供的代码是一模一样的。getSectionData也有默认实现,不过它和我demo的代码有点不一样,它的默认实现是这样的:

1
2
3
getSectionData(dataBlob, sectionID ){
return dataBlob[sectionID];
}

如果要去掉,那么我的renderSectionHeader的代码里面,组件的Text部分就不能返回sectionData而是sectionID了(嗯,这个是我自己写demo的时候傻逼了,明明可以直接用sectionID的,懒得改demo了,大家明白意思就行了)。

这里为了让大家能对ListView的实现有更完整的理解,所以我一开始把代码都加上去了。如果没有其他定制需求,那么上面说的4个地方,你都可以删掉,使用ListView提供的默认实现。即便有定制需求,相信理解完全的你们也可以随便修改相关的代码。

属性之其他

renderSeparator
在每行之间夹一个类似分割线的组件,用法和renderHeader类似,写个组件方法,比如名为

1
2
3
4
5
6
7
renderSeparator(sectionID, rowID, highlightRow){
return(
<TouchableHighlight style={styles.separator} underlayColor='#dddddd'>
<Text>separator[{sectionID}][{rowID}]</Text>
</TouchableHighlight>
);
}

然后在ListView方面里面调用即可:

1
renderSeparator={this.renderSeparator}

不过你这样直接调用的话,会有个黄色警告出来。

然后我想消灭这个警告,却发现这特么是个坑,我网上搜了一圈儿,没看到有能用的办法能取消这个警告。 (哪位仁兄知道怎么解决还望留言)
唔,找到方法了,在返回的组件那里加个key:

1
2
3
4
5
6
7
renderSeparator(sectionID, rowID, highlightRow){
return(
<TouchableHighlight key={sectionID+rowID} style={styles.separator} underlayColor='#dddddd'>
<Text>separator[{sectionID}][{rowID}]</Text>
</TouchableHighlight>
);
}

至于为什么key是{sectionID+rowID},恐怕只有问源码了(现在源码看得一知半解),我也是从别人那里问来,试验成功。
追加:key={${sectionID}-${rowID}} 也是可以的,这是官网文档示例写的,用这个吧。

initialListSize
官方文档解释是:指定在组件刚挂载的时候渲染多少行数据。我测试了一下,比如设置为1,进入页面的时候能比较明显的看到先显示了第一个section的第一个行内容,然后才把后面剩下的给显示出来,影响用户体验。所以这个你自己看设计稿里面一屏上有多少行来设置吧。不设置的话,默认是10。
onEndReached
onEndReachedThreshold
这两个一起的,先设置好onEndReachedThreshold数据(这个是点数,比如iphone6的高度就是667个点),当屏幕底部距离列表底部距离为这个点数的时候,就会调用onEndReached指定的方法。就是说,你设置onEndReachedThreshold 的值为100,列表底部在屏幕下面(没显示出来),距离屏幕底部还有100点的距离时,就会触发onEndReached。不太好表达,试试就知道了,另外那个值可以设置为负的。
pageSize
每次事件循环(帧)时渲染的行数。默认是1,官方解释说,如果你的row并不独占一行,比如设定row的样式为小方块,屏幕上的一行可以排下好几个row。像这种情况,就需要把pageSize改高点,我大概测试了一下,好像并没有什么视觉上明显的不同。
renderScrollComponent
官方解释:指定一个函数,在其中返回一个可以滚动的组件。ListView将会在该组件内部进行渲染。默认情况下会返回一个包含指定属性的ScrollView。
说实话我没看懂这个解释,网上也没找到除了官方解释之外的详细解释,然后在源代码里面翻了一下,我理解成这样:ListView本来就是基于Scrollview做出来的一个特例。这个renderScrollComponent就是指定它是基于哪个Scrollview来渲染,默认实现就是一个最简单的Scrollview,代码是这样的:

1
renderScrollComponent: props => <ScrollView {...props} />

如果你想让你的Listview基于另外某个定制的ScrollView来实现,那就用这个属性另外指定一个ScrollView吧(应该是这个意思,不对的地方请指正)。

未完待续
scrollRenderAheadDistance
onChangeVisibleRows
removeClippedSubviews