原文:ListView
ListView是最常用的可滚动Widget,它可以沿着一个方向限行排布所有子Widget。
构造函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
ListView({
...
//可滚动widget公共参数
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
//ListView各个构造函数的共同参数
double itemExtent,
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
//子widget列表
List<Widget> children = const <Widget>[],
})
|
上面参数分为两组,第一组是可滚动Widget公共参数,第二组是ListView各个构造函数(ListView有多个构造函数)的公共参数。
可滚动Widget公共参数介绍见滚动监听及控制ScrollController
下面是ListView各个构造函数的公共参数:
- itemExtent: 该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的”长度”是指滚动方向上子widget的长度,即如果滚动方向是垂直方向,则itemExtent代表子widget的高度,如果滚动方向为水平方向,则itemExtent代表子widget的长度。在ListView中,指定itemExtent比让子widget自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而不是总是动态去计算,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
- shrinkWrap:该属性表示是否根据子widget的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
- addAutomaticKeepAlives: 该属性表示是否将列表项(子widget)包裹在AutomaticKeepAlive widget中;典型的,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时该列表不会被GC,它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
- addRepaintBoundaries: 该属性表示是否将列表项包裹在RepaintBoundary中。当可滚动widget滚动时,将列表项包裹在RepaintBoundary中可以避免列表重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlives一样,如果列表项自己维护其keepAlive状态时,那么该参数必须为false。
构造函数
默认构造函数
默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子widget的情况,因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子widget真正显示的时候再创建。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。下面是一个例子:
1
2
3
4
5
6
7
8
9
10
|
ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
],
);
|
注意:可滚动widget通过一个List来作为其children属性时,只使用于widget比较少的情况,这是一个通用规律,并非ListView自己的特性,像GridView也是如此。
ListView.builder
ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子widget真正显示的适合才会被创建。下面看一下ListView.builder的核心参数列表。
1
2
3
4
5
6
7
|
ListView.builder({
// ListView公共参数已省略
...·
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
|
- itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
- itemCount: 列表项的数量,如果为null,则为无限列表。
看一个例子:
1
2
3
4
5
6
7
|
ListView.builder(
itemCount: 100,
itemExtent: 50.0, //强制高度为50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);
|

ListView.separated
ListView.separated可以生成列表项之间的分割器,它除了比ListView.build多了一个separatorBuilder参数,该参数是一个分割器。下面我们一个例子:奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class ListView3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
//下划线widget预定义以供复用。
Widget divider1=Divider(color: Colors.blue,);
Widget divider2=Divider(color: Colors.green);
return ListView.separated(
itemCount: 100,
//列表项构造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器构造器
separatorBuilder: (BuildContext context, int index) {
return index%2==0?divider1:divider2;
},
);
}
}
|

实例:无限加载列表
假设我们要从数据源异步分批拉去一些数据,然后用ListView显示,当我们滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个loading,拉取成功后将输入插入列表了;如果不需要再去拉去,则在表尾提示“没有更多”。代码如下:
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
class InfiniteListView extends StatefulWidget {
@override
_InfiniteListViewState createState() => new _InfiniteListViewState();
}
class _InfiniteListViewState extends State<InfiniteListView> {
static const loadingTag = "##loading##"; //表尾标记
var _words = <String>[loadingTag];
@override
void initState() {
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: _words.length,
itemBuilder: (context, index) {
//如果到了表尾
if (_words[index] == loadingTag) {
//不足100条,继续获取数据
if (_words.length - 1 < 100) {
//获取数据
_retrieveData();
//加载时显示loading
return Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(strokeWidth: 2.0)
),
);
} else {
//已经加载了100条数据,不再获取数据。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: Text("没有更多了", style: TextStyle(color: Colors.grey),)
);
}
}
//显示单词列表项
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (context, index) => Divider(height: .0),
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
_words.insertAll(_words.length - 1,
//每次生成20个单词
generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
);
setState(() {
//重新构建列表
});
});
}
}
|

添加固定表头
很多时候我们需要给列表添加一个固定的表头,比如我们想实现一个商品列表,需要在列表顶部添加一个“商品列表”标题,效果如下:
我们按照之前经验,写出如下代码:
1
2
3
4
5
6
7
8
9
|
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
]);
}
|
然后运行,发下没有出现我们期望的效果,相反触发了一个异常:
1
2
|
Error caught by rendering library, thrown during performResize()。
Vertical viewport was given unbounded height ...
|
从异常信息中,我们可是因为ListView高度边界无法确定引起的,所以解决的办法也很明显,我们需要给ListView指定边界,我们可以通过SizedBox指定一个列表高度看是否生效:
1
2
3
4
5
6
7
8
|
... //省略无关代码
SizedBox(
height: 400, //指定列表高度为400
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
...
|
可以看到效果效果实现了额,但是这种方法并不优雅。优雅方式使用Column+Expanded来实现,代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
Expanded(
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
]);
}
|