html原生listview,使用 JS 构建跨平台的原生应用:ListView 组件介绍

a4c26d1e5885305701be709a3d33442f.png

背景

滚动列表 几乎是移动开发中用途最广的

UI 组件,其重要性不言而喻。由于平台差异性,React Native 中的滚动列表组件 ListView 并没有直接映射为

Android 中的 ListView 或 iOS 中的

UITableView,而是在 ScrollView 的基础上使用

JS 做了一次封装。这样,滚动体验部分由 Native 负责,而 React 部分则专注于组件何时渲染、如何渲染等问题。

ListView 的基本设计原则是 “数据和展现相隔离” ,如下图所示。给我们带来的好处就是,我们只需关注数据的组织方式,任何对数据的操作都会自动渲染出对应的展现。

a4c26d1e5885305701be709a3d33442f.png

数据

ListView 在创建时需要绑定数据源,类似于 Android 中的

Adaptor。数据源内部保存了展现需要的初始数据 _dataBlob

,它是一个纯粹的对象或数组。列表可以带 SectionHeader (即列表中某一段的标题部分)也可以不带,本质上相同。数据源默认的格式有三个维度:

第一个维度是 sectionId ,标识属于哪一段,

可以手动指定或隐式地使用数组索引或对象的 key 值;

第二个维度是 rowId ,标识某个数据段下的某一个行,同样可以手动指定或隐式地使用数组索引或对象的

key 值;

第三个维度是具体的数据对象,根据实际的需要而定。

需要注意的是,上面只是 默认的数据格式,如果它不符合实际的需求, 完全可以使用自定义的数据结构 。唯一的区别就是需要额外指定给

ListView 数据源中哪些是 id,哪些是 rowData。

a4c26d1e5885305701be709a3d33442f.png

DataSource 的构造函数接收以下几个参数:

rowHasChanged: 用于在数据变化的时候,计算出变化的部分,在更新时只渲染脏数据;

sectionHeaderHasChanged: 同理,在列表带分段标题时需要实现;

getRowData/getSectionHeaderData:

如果遵循默认的数据源格式,这两个方法就没有必要实现,用内部默认的即可;而当数据源格式是自定义时,需要手动实现这两个方法。

如文档中一般介绍的那样,DataSource 的初始化一般在 getInitialState 方法中:

JavaScript

1

2

3

4

5

6

getInitialState:

function()

{

var

ds = new ListView.DataSource({rowHasChanged:

(r1,

r2)

=>

r1 !== r2});

return

{

dataSource:

ds.cloneWithRows(['row

1',

'row 2']),

};

}

在初次见到这种写法时,心中其实疑惑这里为什么不一次性把整个对象 new 出来,而要拆分为两步?

首先,如前所述,DataSource 的构造函数里的参数只有四个,并不能直接传入数据对象。其次,一个页面中的滚动列表常常要不时修改数据,比如:

边滚动边添加列表元素;

展现搜索结果时,当搜索条件变化后,列表对应的数据对象需要重置;

对列表中的结果进行筛选、排序等操作。

在这些情况下,数据源需要做调整,但上述写法可以使得我们在调整时,无需重新定义 rowHasChanged

等比较函数,只需变化真实的数据对象即可。

展现

基本用法

数据源确定后,下一个工作就是列表的渲染。在渲染时发挥重要作用的是 renderRow 属性,它接收数据源中保存的数据对象,并通过返回值确定该行该如何进行展现。我们可以对所有行统一进行展现,也可以根据里面的字段做出不同的展现。在列表包含

sectionHeader 时,还需要实现 renderSectionHeader 方法。一个简单的例子如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

var postDemo = React.createClass({

getInitialState:

function()

{

var

ds = new ListView.DataSource({

rowHasChanged:

(r1,

r2)

=>

r1 !== r2,

sectionHeaderHasChanged:

(s1,

s2)

=>

s1 !== s2

});

var

dataBlob = {...}

return

{

dataSource:

ds.cloneWithRowsAndSections(dataBlob)

}

},

renderRow:

function(rowData,

sectionId,

rowId)

{

return

(<View

style={styles.row}>

<Image

source={{uri:

'http:' + rowData.pic}}

style={styles.image}

/>

<View

style={styles.rightSection}>

<Text

style={styles.title}>{rowData.title}</Text>

<Text

style={styles.priceInfo}>当前价:

¥{rowData.price}</Text>

</View>

</View>);

},

renderSectionHeader:

function(sectionData,

sectionId)

{

return

(<View>

<Text

style={styles.sectionHeaderStyle}>{sectionId}</Text>

</View>);

},

render:

function()

{

return

(

<ListView

style={styles.listview}

dataSource={this.state.dataSource}

renderSectionHeader={this.renderSectionHeader}

renderRow={this.renderRow}/>

);

}

});

Demo 运行的效果如下:

a4c26d1e5885305701be709a3d33442f.png

分页

除了简单的渲染之外,另外一个要考虑的问题就是 当数据量很大的时候如何分页加载 。这种情形分两种情况考虑:

数据一次性拿到,边滚动边加载

数据不是一次性拿到,而是有可能分屏取数据

对于第一种情况,在 ListView 内部其实已经做了分页的处理:

ListView 内部通过 curRenderedRowsCount 状态保存已渲染的行数;

初始状态下,要加载的数据条数等于 initialListSize(默认为

10 条);

在滚动时检测当前滚动的位置和最底部的距离,如果小于 scrollRenderAheadDistance (默认为

1000),就更新curRenderedRowsCount ,在它原有值基础上加 pageSize 个(默认为

1 条);

由于属性变化,触发了 ListView 重新的 render 。在渲染过程中,curRenderedRowsCount 起到截断数据的作用,React

的 diff 算法使得只有新加入的数据才会渲染到了界面上。

整个过程类似于 Web 端懒加载机制,即 每次在和底部的距离达到一个阈值时,加载接下来的

pageSize 个数据 。

对于第二种情况,ListView 提供了相关的属性:

onEndReachedThreshold,在滚动即将到达底部时触发;

onEndReached,在已经到达底部时触发;

我们可以在这两个方法中调用接口去拿数据,取到数据后再更新数据源。

多列

很多页面中的列表并非单列的,如手淘搜索结果页里,商品分两列并排展示。乍一看似乎要做出不少调整,但实际上只通过布局即可达到相关效果。ListView

并没有强制要求一个 rowData 在展示时一定要占满一行,在多列的情况下,我们适时调整每个 rowData 占据的宽度即可。

由于 React Native 使用 Flexbox

进行布局,在实现多列时,主要用到的是 flexWrap:wrap 属性:它的效果类似于

float,即水平地排列每一项,当放不下时进行折行处理。在设置每行视图占据一半宽度后就达到了两列的效果,多列的类似。

a4c26d1e5885305701be709a3d33442f.png

滚动

ListView 只是整合了数据和展现,但实际滚动的功能还是由 ScrollView 全权负责。ScrollView

实现完全和平台相关:在 iOS 上,它映射为 RCTScrollView;在

Android 上,它映射为 RCTScrollView 和 AndroidHorizontalScrollView。

React Native 让不同端上的技术融合在了一起,同时也给开发人员提出了更高的要求。以 ScrollView

为例,大量的属性其实原封不动映射给了 UIScrollView,这就意味着如果想再深入地研究下去,必须对客户端相关技术有足够了解。无论是前端还是客户端,跳出自己熟悉的那片领域也许才是更进一步的关键。

谈到滚动,有一点不得不说的就是 列表的无限加载,这牵涉到滚动的性能。

Github 上的这个 issue对此展开了热烈的讨论。其中有人就提到,数据量很大情况下,ListView 在加载时所占用的

CPU 和内存会大大增加,滚动到最后就导致了应用 crash。

为此,ListView 中新添加了一个实验性的属性: removeClippedSubviews ,它能在滚动时及时删掉列表中处于视窗的之外的行,以此达到降低内存消耗的目的。不幸的是,即使设置了这个属性,程序虽然各项占用减少了不少,但还是没避免崩溃的命运。处于好奇,我也在最新版的

ListView 基础上做了简单尝试,不断加载一个无限大的列表,但并没有出现崩溃的情况:

即使加载了 3000、4000 行,Android 真机、iOS 真机和 iOS 模拟器上都没有崩溃;

Android 上明显感到数据加载有 阶段性的延时 ,即滚动一定程度后,再次滚动数据始终加载不出来或要等一段时间才加载出来,体验较差;iOS

相比要流畅的多;

但不崩溃并非最终的目的,很多 React Native 使用者都在试图改进 ListView 的性能表现,相比于直接使用 Native

端的组件,ListView 性能还是差强人意,有很大优化空间。

总结

ListView 并没有创造出新的东西,它只是集各家所长,很好地将 React 的视图渲染和 Native

端很成熟的滚动机制融合在了一起,使用起来和其他组件无差,静态地定义展现、动态地组织数据,是给人带来的直观感受。本文仅对

ListView 基础用法作了简要介绍,更为细致的点还是要在实际使用的过程中去发现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值