本文章代码已上传github,点击下载,喜欢别忘了给star哦
Hello,非常欢迎大家关注React全栈开发。上一期我们介绍了React-Native和Native之间的通信,然后刚好最近在做一个ListView排版的项目,这期索性就为大家献上一个完整的ListView排版过程。
一.什么是ListView
关于ListView官方文档有这么一段话:
ListView - A core component designed for efficient display of vertically scrolling lists of changing data. The minimal API is to create aListView.DataSource, populate it with a simple array of data blobs, and instantiate aListViewcomponent with that data source and arenderRowcallback which takes a blob from the data array and returns a renderable component.
作为核心组件,ListView(tableView)可以说是日常开发最常用的控件之一。会话列表(微信),商品列表(淘宝),问题列表(汽车大师)等等各种列表均用到了ListView。React-Native的ListView对于原生的支持还是比较不错的,但是和原生的用法略有不同。官方文档给出的例子不是特别详细,所以这是我写这篇文章的初衷,帮助更多的人真正了解和运用这个组件
二.ListView dataSource
本段我们主要讲解dataSource。和Native一样,ListView需要指定数据的来源。传入数据必须是数组,或者是字典里面嵌套数组。不同的是,你无法像原生一样去显示指定section和row的个数。系统会根据你传入的数据自动生成section和row。每一个字典的key会被ListView拆分成一个section(小节,这里的section和原生的一样),如果你不指定sectionId,则key就是sectionId。
官方希望我们在构造函数中指定ListView的取值策略。在使用dataSource时,我们需要先new一个dataSource对象,形如下。
constructor(props) {
super(props);
var ds =new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
}
构造函数可以接受下列四种参数(都是可选):
1.getRowData(dataBlob, sectionID, rowID):表明我们将以何种方式从dataBlob(数据源)中提取出rowData,sectionID用于指定每一个section的标题名(在renderRow,renderHeader等方法中会默认拆开并作为参数传入)
2.getSectionHeaderData(dataBlob, sectionID):表明我们将以何种方式从dataBlob(数据源)中提取出HeaderData。HeaderData用于给每一个sectionHeader赋值
3.rowHasChanged(prevRowData, nextRowData):指定我们更新row的策略,一般来说都是prevRowData和nextRowData不相等时更新row
4.sectionHeaderHasChanged(prevSectionData, nextSectionData):指定我们更新sectionHeader的策略,一般如果用到sectionData的时候才指定
本次项目中我并没有手动指定dataBlob提取策略,而是使用默认的方式,默认方式目前支持如下的数据结构
{ sectionID_1: { rowID_1: rowData1, ... }, ... }
或者:
{ sectionID_1: [ rowData1, rowData2, ... ], ... }
或者:
[ [ rowData1, rowData2, ... ], ... ]
大多数情况下我们会用到第二种(字典套数组)和第三种(纯数组),每一个数组对应的key系统会自动提取成sectionId。然后我们可以根据sectionId来判断所加载的section。我们使用下面的方法为dataSource传递数据。如果是纯数组(无section),使用
cloneWithRows(dataBlob, rowIdentities)
该方法接收两个参数,dataBlob是原始数据源。在没有section的时候使用此方法,传入一个数组。rowIdentities为可选类型,为数据源的每一项指明一个id。默认的id为字符串'0','1','2'...dataBlob.count。
如果是字典套数组的结构(有section),我们使用下面的方法为数据源赋值。sectionIdentities和rowIdentities都是可选类型。
cloneWithRowsAndSections(dataBlob, sectionIdentities, rowIdentities)
说了这么多,现在我们来具体演示一下。首先我们打开终端执行react-native init ListViewDemo生成一个全新的React-Native工程,打开你的index.ios.js。在你的class里面创建构造方法,并且键入如下代码
第一步我们创建了DataSource对象,提供了row更新的策略。然后我们在初始化state的时候,初始化一个dataSource和一个data属性,后面构建ListView的时候需要用到。然后我们在render方法里面构建ListView,如下
我们调用了dataSource对象提供的cloneWithRows方法给ListView的dataSource赋值,并传入了数据源data。然后在renderRow方法中返回一个Text控件,将每一行的内容和rowId打印出来。
这是一个最简单的ListView实现。下面我们实现一个带有section的ListView。
上面提到过RN会根据你传入的数据源判断是否含有section,并调用cloneWithRowsAndSections把数据源传给ListView。如果你不指定sectionId,则RN会默认按之前提到过的数据结构取出相应的sectionId和rowId。下面我们来演示一下。
我们来把数据源改变一下,传入如下形式数据。注意,如果使用cloneWithRowsAndSections传入数据源,必须在构造方法里面传入sectionHeaderHasChanged方法
然后我们在List组件中使用cloneWithRowsAndSections替换原来的cloneWithRows方法,并增加sectionId的输出
根据默认提取规则,本数据将会生成5个section。a、b、c、d、e分别为其对应的sectionId。我们并没有指定rowId,那么每一个section里将有两个row,rowId分别为默认的0和1。现在我们来运行程序,看看结果如何。
结果也正如我们所预料的那样。
三.实例演示
1.具体页面搭建
下面我来带着大家做一个简单的ListView实现列表的数据展示,主要目的是帮助大家熟悉和使用ListView。首先,我们写一个网络请求的方法,这里我们利用es7 Async特性搭配fetch很方便的写出来
在这里,我们有4个url,分别返回不同的数据。本例中我们将分别请求四个url,最后将返回的数据合并在一个searchData对象里。利用async特性,因为async函数返回的是一个Promise对象,我们可以在后面很方便的在拿到数据以后进行二次fetch,所以我们重新构建一个async函数,用来合并每一次fetch以后的数据。
service,search,mechanic,article分别是4个url。在拿到数据以后,我们指定了4个key,分别对应4组数据。这么写的目的是方便我们在调用cloneWithRowsAndSections的时候能根据key自动生成sectionId。最后我们打印了searchData,数据格式大概是这个样子的
确定数据无误后,我们调用setState方法,把数据传入state。然后在ListView中,使用cloneWithRowsAndSections方法将state里面的数据传入ListView。
在这里注意,我们需要判断this.state.data是否为空,避免data为空报错。ref返回一个函数,参数为该组件的引用。在这里我们把引用赋予_listView变量。renderRow用于把从数据源接收的一条数据rowData和所在的sectionId,rowId作为参数传入,返回一个用该数据渲染的组件。基于之前的数据结构,我们可以很快得知此处的sectionId有4个,分别为service,question,mechainc,article。rowId没有设置,所以默认是'0','1'.....sectionData.length。然后,在row的渲染函数中,我们根据判断不同的sectionId和rowid来渲染不同的row。现在来看一下渲染函数中的逻辑。
需要注意的是RN给我们提供了渲染SectionHeader的函数,但并没有提供渲染SectionFooter的函数,如果我们想渲染Footer,可以在renderRow里面实现。具体做法是我们在数据源对应section的数组最后增加一个元素,使得该section在渲染的时候多渲染一行row。这样,我们就渲染出来了一个带4个section的ListView
下面我们在相关咨询尾部加一个footer,设计初衷是用于跳转到提问页面。我们需要找到相关咨询对应的section。然后我们使用数组展开运算符,在questions_data.questions的尾部追加了一个footer字符串(也可以用push,这里为了展示展开运算符)。这时候questions_data.questions变成了3个。然后在我们给questions section赋值的时候,判断rowId,如果为2,则渲染提交问题的按钮row。
最后,row添加成功,达到了和footer一样的效果
2.上下拉刷新的实现
1)下拉刷新:
RN给我们提供了现成的上拉刷新组件RefreshControl,该组件是由原生模块实现,并嵌到RN中
refreshControl element#
指定RefreshControl组件,用于为ScrollView提供下拉刷新功能。
我们在ListView的refreshControl属性中指定该组件
RefreshControl属性
onRefresh function#
在视图开始刷新时调用。指定一个刷新方法,在下拉开始时调用。在这里我们指定网络请求的方法
refreshing bool#
视图是否应该在刷新时显示指示器。一般用state||props(redux)来控制,在数据加载的时候设置为true,加载完毕以后设置为false
剩下一些UI的属性大家可以自行查阅官方文档。当然使用之前别忘记引用它。它存在于react-native的基础API中。
给ListView添加完毕以后,我们在_combineData方法里面控制它的开关。最后下拉效果如下
如果需要深度定制效果的,可以参照refreshControl去用原生实现RN的模块,这一块内容在RN和Native之间通信的时候有提到
2)上拉加载更多:
目前RN官方并没有上拉加载更多组件,这次我简单的实现了一个。
首先你需要在自定义一个小菊花组件,或者使用RN提供的ActivityIndicatorIOS。该组件暂时只支持iOS。想要跨平台的话还是自己动手写吧O(∩_∩)O~。
很简单,就是一个Animated实现一个图片的旋转动画,根据父组件传过来的属性loading来控制旋转或者不显示
然后我们来实现onScroll的回调方法。和原生一样,该方法继承于Scrollview组件的。
这里回调函数传递过来的参数是一个事件,我们可以根据e.nativeEvent拿到我们需要的相关属性。
这里我们判断contentOffset.y > 0并且contentOffset.y + scrollView.height > contentSize.height + 100(相当于在scrollview的最底部在往下滑动100,但是安卓的ListView没有弹性机制,这里不用加这100。你也可以自己来定义这个高度)。如果条件成立就进行加载。这里要注意我们需要一个变量来阻挡多余的回调,这里我们用一个全局变量canLoad来处理。再补充一点,这里我们使用的是Redux的异步action来请求网络,你也可以用state来控制,效果都是一样的。当网络请求成功,回调过来,把state.loadMore设置为false,就这么简单。
四.总结
本文只是介绍了ListView的基本用法,用在实际开发中的经验去弥补官方文档的空缺,还有一些更加深入的东西比如性能优化我会在后期慢慢挖掘然后奉献给大家。其实RN在原生的表现的话,虽然说达不到100%完美,但是也是可以接受的。目前安卓平台还遗留众多问题。比如TextInput的returnKeyType属性在安卓不好用,以及alignItems:'center'在安卓无效之类的坑,还有一些诸如GifImage的奇葩崩溃。但是确实能节省不少开发成本,再加上热更新。
总而言之,RN还是值得你去尝试,并且付出精力的一门技术。
另外,小弟和朋友一起开了一个公众号,主要用于发布React全栈的一些相关文章以及技术交流,欢迎大家关注。我的微信wsdlljjj