约定:后面如果我们说一个组件是Sliver 则表示它是基于Sliver布局的组件,同理,说一个组件是 RenderBox,则代表它是基于盒模型布局的组件,并不是说它就是 RenderBox 类的实例。
ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。我们看看ListView的默认构造函数定义:
ListView({
...
//可滚动widget公共参数
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
EdgeInsetsGeometry? padding,
//ListView各个构造函数的共同参数
double? itemExtent,
Widget? prototypeItem, //列表项原型,后面解释
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double? cacheExtent, // 预渲染区域长度
//子widget列表
List<Widget> children = const <Widget>[],
})
上面参数分为两组:第一组是可滚动组件的公共参数,本章第一节中已经介绍过,不再赘述;第二组是ListView各个构造函数(ListView有多个构造函数)的共同参数,我们重点来看看这些参数,:
itemExtent
:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会有更好的性能,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。prototypeItem
:如果我们知道列表中的所有列表项长度都相同但不知道具体是多少,这时我们可以指定一个列表项,该列表项被称为 prototypeItem(列表项原型)。指定 prototypeItem 后,可滚动组件会在 layout 时计算一次它延主轴方向的长度,这样也就预先知道了所有列表项的延主轴方向的长度,所以和指定 itemExtent 一样,指定 prototypeItem 会有更好的性能。注意,itemExtent 和prototypeItem 互斥,不能同时指定它们。shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。addAutomaticKeepAlives
:该属性我们将在介绍 PageView 组件时详细解释。addRepaintBoundaries
:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。RepaintBoundary 读者可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效(具体原因会在本书后面 Flutter 绘制原理相关章节中介绍)。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为 false。
注意:上面这些参数并非
ListView
特有,在本章后面介绍的其它可滚动组件也可能会拥有这些参数,它们的含义是相同的。
1. 默认构造函数
默认构造函数有一个children参数,它接受一个Widget列表(List<Widget>)。这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder 按需动态构建列表项
注意,虽然这种方式将所有children一次性传递给 ListView,但子组件仍然是在需要时才会加载(build(如有)、布局、绘制),也就是说通过默认构造函数构建的 ListView 也是基于 Sliver 的列表懒加载模型。
示例1
class ListViewDemo extends StatelessWidget {
ListViewDemo({
Key? key,
}) : super(key: key);
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
shrinkWrap: true,
children: [
Padding(
padding: EdgeInsets.all(8),
child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle)),
Padding(
padding: EdgeInsets.all(8),
child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。",
style: textStyle)),
Padding(
padding: EdgeInsets.all(8),
child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。。", style: textStyle))
],
);
}
}
示例2
class ListViewDemo1 extends StatelessWidget {
const ListViewDemo1({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: List.generate(100, (index) {
// ListTile 小分片
return ListTile(
leading: Icon(Icons.people),
trailing: Icon(Icons.delete),
title: Text(
"联系人 ${index + 1}",
style: TextStyle(color: Colors.orange, fontSize: 20),
),
subtitle: Text(
"联系方式: 18826625555",
style: TextStyle(color: Colors.grey, fontSize: 16),
),
);
}),
);
}
}
2. ListView.builder
ListView.builder适合列表项比较多或者列表项不确定的情况,下面看一下ListView.builder的核心参数列表
ListView.builder({
// ListView公共参数已省略
...
required IndexedWidgetBuilder itemBuilder,
int itemCount, // item数量
...
})
-
itemBuilder
:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
*itemCount
:列表项的数量,如果为null,则为无限列表。
示例1
class ListViewBuilderDemo extends StatelessWidget {
const ListViewBuilderDemo({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 50, // 主轴方向高度
itemBuilder: (BuildContext ctx, int index) {
return Text("Item ${index + 1}");
},
);
}
}
3. ListView.separated
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器
示例1
class ListViewSeparatedDemo extends StatelessWidget {
const ListViewSeparatedDemo({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Icon(Icons.people),
trailing: Icon(Icons.delete),
title: Text(
"联系人 ${index + 1}",
style: TextStyle(fontSize: 20),
),
);
},
separatorBuilder: (BuildContext ctx, int index) {
return Divider(
height: 20, // Divider 高度,不是线的高度
thickness: 5, // 线的高度
color: index % 2 == 0 ? Colors.orange : Colors.blue,
indent: 16, // 左侧间距
endIndent: 16, // 右侧间距
);
},
itemCount: 100);
}
}
4. ListView.custom
我们看下ListView.custom的定义
const ListView.custom({
...
required this.childrenDelegate,
...
})
ListView.custom
主要是传一个SliverChildDelegate
代理, SliverChildDelegate
是abstract(抽象类),它有两个子类SliverChildBuilderDelegate
、SliverChildListDelegate
** SliverChildListDelegate**
定义如下:
SliverChildListDelegate(
this.children, {
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
})
children
是必传的,是不是很眼熟,ListView默认构造函数里也是传一个children,实际上ListView默认构造函数中是通过children创建一个SliverChildListDelegate的
ListView构造函数处理children源码如下:
childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
** SliverChildBuilderDelegate**
定义如下:
const SliverChildBuilderDelegate(
this.builder, {
this.findChildIndexCallback,
this.childCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
})
builder
是必传的,是个NullableIndexedWidgetBuilder类型,和IndexedWidgetBuilder类似,ListView.builder源码中是通过builder创建个SliverChildBuilderDelegate的
ListView.builder 处理 itemBuilder 源码如下
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
示例1 - SliverChildListDelegate
class ListViewCustomDemo1 extends StatelessWidget {
const ListViewCustomDemo1({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.custom(
itemExtent: 50, // 高度
childrenDelegate: SliverChildListDelegate(
List.generate(100, (index) {
return ListTile(
title: Text("商品Item ${index + 1}",
style: TextStyle(color: Colors.red, fontSize: 18)),
trailing: Icon(
Icons.favorite,
color: Colors.white,
),
);
}),
),
);
}
}
示例2 - SliverChildBuilderDelegate
class ListViewCustomDemo2 extends StatelessWidget {
const ListViewCustomDemo2({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.custom(
itemExtent: 100,
childrenDelegate:
SliverChildBuilderDelegate((BuildContext ctx, int index) {
return Container(
color: Color.fromARGB(Random().nextInt(256), Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}, childCount: 100),
);
}
}
5. 固定高度列表
默认情况下,列表中的Item的高度是随内容自适应的。
但给列表指定 itemExtent 或 prototypeItem 会有更高的性能,所以当我们知道列表项的高度都相同时,强烈建议指定 itemExtent 或 prototypeItem
示例1
class ListViewFixedExtentDemo1 extends StatelessWidget {
const ListViewFixedExtentDemo1({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 56,
// prototypeItem: ListTile(title: Text("Item")),
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
title: Text("Item $index"),
);
},
);
}
}
自定义个LayoutLogPrint组件,在布局时可以打印当前上下文中父组件给子组件的约束信息
完整代码如下
class ListViewFixedExtentDemo2 extends StatelessWidget {
const ListViewFixedExtentDemo2({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
prototypeItem: ListTile(title: Text("1")),
// itemExtent: 56,
itemBuilder: (context, index) {
//LayoutLogPrint是一个自定义组件,在布局时可以打印当前上下文中父组件给子组件的约束信息
return LayoutLogPrint(
tag: index,
child: ListTile(title: Text("$index")),
);
},
);
}
}
class LayoutLogPrint<T> extends StatelessWidget {
final Widget child;
final T? tag;
const LayoutLogPrint({Key? key, required this.child, this.tag})
: super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (_, constraints) {
// assert在编译release版本时会被去除
assert(() {
print('${tag ?? key ?? child}: $constraints');
return true;
}());
return child;
});
}
}
因为列表项都是一个 ListTile,高度相同,但是我们不知道 ListTile 的高度是多少,所以指定了prototypeItem ,运行后,控制台打印:
flutter: 0: BoxConstraints(w=375.0, h=56.0)
flutter: 1: BoxConstraints(w=375.0, h=56.0)
flutter: 2: BoxConstraints(w=375.0, h=56.0)
flutter: 3: BoxConstraints(w=375.0, h=56.0)
...
可见 ListTile 的高度是 56 ,指定itemExtent为56也是可以的,建议优先指定原型,这样的话在列表项布局修改后,仍然可以正常工作(前提是每个列表项的高度相同)
如果本例中不指定 itemExtent 或 prototypeItem ,我们看看控制台日志信息
flutter: 0: BoxConstraints(w=375.0, 0.0<=h<=Infinity)
flutter: 1: BoxConstraints(w=375.0, 0.0<=h<=Infinity)
flutter: 2: BoxConstraints(w=375.0, 0.0<=h<=Infinity)
flutter: 3: BoxConstraints(w=375.0, 0.0<=h<=Infinity)
...
可以发现,列表不知道列表项的具体高度,高度约束变为 0.0 到 Infinity。
6.列表的原理
ListView 内部组合了 Scrollable、Viewport 和 Sliver,需要注意:
- ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。
- 一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。
- ListView 的 Sliver 默认是 SliverList,如果指定了
itemExtent
,则会使用 SliverFixedExtentList;如果prototypeItem
属性不为空,则会使用 SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。
- ListView 的 Sliver 默认是 SliverList,如果指定了
我们解释下ListView的Sliver
ListView 是继承于BoxScrollView,BoxScrollView继承于ScrollView,ScrollView继承于StatelessWidget,因而ScrollView是我们需要读的最深层级,在ScrollView的方法中,是通过buildSlivers来获取slivers的,而buildSlivers方法在ScrollView中是抽象方法,因而它的子类需要实现。现在我们需要看下BoxScrollView中关于buildSlivers的实现,在BoxScrollView的 buildSlivers方法中是通过buildChildLayout来获取Sliver,而buildChildLayout在BoxScrollView中也是抽象方法,因而我们去看BoxScrollView子类ListView中buildChildLayout的实现,ListView中buildChildLayout源码如下
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
} else if (prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
prototypeItem: prototypeItem!,
);
}
return SliverList(delegate: childrenDelegate);
}
默认情况下 ListView的Sliver是SliverList,在itemExtent不为空时,是SliverFixedExtentList,在prototypeItem不为空时是SliverPrototypeExtentList
7 实例:无限加载列表
class _MSHomePageContentState extends State<MSHomePageContent> {
static const loadingTag = "##loading##";
var _words = <String>[loadingTag];
@override
void initState() {
super.initState();
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext ctx, int index) {
if (_words[index] == loadingTag) {
if (_words.length <= 100) {
_retrieveData();
// 加载时显示loading
return Container(
padding: EdgeInsets.all(16),
alignment: Alignment.center,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2.0),
),
);
} else {
// 已经加载了100条数据,不再获取数据。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
child: Text("没有更多", style: TextStyle(color: Colors.grey)),
);
}
}
// 显示单词列表
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (BuildContext ctx, int index) {
return Divider(
color: Colors.amber, thickness: 3, indent: 16, endIndent: 16);
},
itemCount: _words.length);
}
_retrieveData() {
Future.delayed(Duration(seconds: 3)).then((value) {
// 每次生成20对单词
List<String> newData =
generateWordPairs().take(20).map((e) => e.asSnakeCase).toList();
_words.insertAll(_words.length - 1, newData);
setState(() {});
// List<WordPair> newData = generateWordPairs().take(10).toList();
// List<String> data = newData.map((e) => e.asPascalCase).toList();
// _words.addAll(data);
});
}
}