React Native之如何写一个ListView

写 RN 也有段时间了,今天抽空就对 RN 当中的 ListView 进行一个简单介绍,主要介绍使用当中的一些坑。

写一个简单的 ListView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyComponent extends Component {
constructor() {
super();
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds.cloneWithRows(['row 1', 'row 2']),
};
}

render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}
/>
);
}
}

以上代码就可以绘制一个简单的 ListView 了,但是我们需要注意dataSource: ds.cloneWithRows(['row 1', 'row 2'])这一行,因为我们的数据一般都是网络请求来的,所以我们一般可能会写成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
listData = [];

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

***省略的网络请求***
listData = ['row 1', 'row 2'];
***省略的更新state***

然而你发现你的 ListView 并没有更新,还是一片空白,当我们把dataSource: ds.cloneWithRows(this.listData)这一行改成dataSource: ds.cloneWithRows(this.listData.slice())你会发现问题解决了,why?

这是因为这样只是一个浅拷贝,虽然数组内容发生了变化,但是ds.cloneWithRows接收到的数据没有发生变,故页面会刷新。当我们用slice时,给它返回一个新值,这样界面就会重新刷新渲染。

写一个带 Section 的 ListView

要写一个带 Section 的 ListView 主要是针对数据源的改造。

DataSource 构造函数可以接受下列四种参数(都是可选):

1
2
3
4
getRowData(dataBlob, sectionID, rowID);
getSectionHeaderData(dataBlob, sectionID);
rowHasChanged(prevRowData, nextRowData);
sectionHeaderHasChanged(prevSectionData, nextSectionData);

如果不提供 getRowData 和 getSectionHeaderData 方法,使用 defaultGetRowData 和 defaultGetSectionHeaderData 来提取数据,默认的提取函数可以处理的数据结构如下:

1
2
3
4
5
{ sectionID_1: { rowID_1: rowData1, ... }, ... }
或者
{ sectionID_1: [ rowData1, rowData2, ... ], ... }
或者
[ [ rowData1, rowData2, ... ], ... ]

我们用第二种数据结构来实现:

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
class MyComponent extends Component {
constructor() {
super();

listData = { "1": [ "11", "12"], "2": [ "21", "22"] };

const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 != s2
});
this.state = {
dataSource: ds.cloneWithRowsAndSections(
this.listData,
Object.keys(this.listData),
Object.keys(this.listData).map(
(sectionId) => Object.keys(
this.listData[sectionId].slice()
)
)
),
};
}

render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}
/>
);
}
}

需要注意的是cloneWithRowsAndSections(dataBlob, sectionIdentities, rowIdentities)方法当中的参数也需要是一个新值,所以rowIdentities参数的数据也需要slice()一下,回传一个新值。

当需要将 sectionHeader 的样式显示出来,还需要在 render 当中添加关于 header 渲染的代码

1
2
3
4
5
6
7
8
9
10
11
render() {
return (
<ListView
renderSectionHeader={
(sectionData, sectionID) => <Text>{sectionID}</Text>
}
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}
/>
);
}

这样一个带 section 的 ListView 就写出来了。

一些注意事项

  1. React Native 不支持随 ListView 滚动的 Header,如果需要,请在每个 section 当中的第一个 Item 当中添加
  2. 如果 ListView 要下拉一下才显示出来,请设置 ListView 的属性removeClippedSubviews={false}
  3. 如果在 iOS 设备上 ListView 距离顶部导航栏有距离,请设置 ListView 的属性automaticallyAdjustContentInsets={false}