約定:后面如果我們說(shuō)一個(gè)組件是Sliver 則表示它是基于Sliver布局的組件剥哑,同理上炎,說(shuō)一個(gè)組件是 RenderBox,則代表它是基于盒模型布局的組件均践,并不是說(shuō)它就是 RenderBox 類的實(shí)例。
ListView是最常用的可滾動(dòng)組件之一摩幔,它可以沿一個(gè)方向線性排布所有子組件彤委,并且它也支持列表項(xiàng)懶加載(在需要時(shí)才會(huì)創(chuàng)建)。我們看看ListView的默認(rèn)構(gòu)造函數(shù)定義:
ListView({
...
//可滾動(dòng)widget公共參數(shù)
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
EdgeInsetsGeometry? padding,
//ListView各個(gè)構(gòu)造函數(shù)的共同參數(shù)
double? itemExtent,
Widget? prototypeItem, //列表項(xiàng)原型或衡,后面解釋
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double? cacheExtent, // 預(yù)渲染區(qū)域長(zhǎng)度
//子widget列表
List<Widget> children = const <Widget>[],
})
上面參數(shù)分為兩組:第一組是可滾動(dòng)組件的公共參數(shù)焦影,本章第一節(jié)中已經(jīng)介紹過(guò),不再贅述封断;第二組是ListView各個(gè)構(gòu)造函數(shù)(ListView有多個(gè)構(gòu)造函數(shù))的共同參數(shù)斯辰,我們重點(diǎn)來(lái)看看這些參數(shù),:
itemExtent
:該參數(shù)如果不為null坡疼,則會(huì)強(qiáng)制children的“長(zhǎng)度”為itemExtent的值彬呻;這里的“長(zhǎng)度”是指滾動(dòng)方向上子組件的長(zhǎng)度,也就是說(shuō)如果滾動(dòng)方向是垂直方向,則itemExtent代表子組件的高度闸氮;如果滾動(dòng)方向?yàn)樗椒较蚣艨觯瑒titemExtent就代表子組件的寬度。在ListView中蒲跨,指定itemExtent比讓子組件自己決定自身長(zhǎng)度會(huì)有更好的性能译断,這是因?yàn)橹付╥temExtent后,滾動(dòng)系統(tǒng)可以提前知道列表的長(zhǎng)度或悲,而無(wú)需每次構(gòu)建子組件時(shí)都去再計(jì)算一下孙咪,尤其是在滾動(dòng)位置頻繁變化時(shí)(滾動(dòng)系統(tǒng)需要頻繁去計(jì)算列表高度)。prototypeItem
:如果我們知道列表中的所有列表項(xiàng)長(zhǎng)度都相同但不知道具體是多少巡语,這時(shí)我們可以指定一個(gè)列表項(xiàng)翎蹈,該列表項(xiàng)被稱為 prototypeItem(列表項(xiàng)原型)。指定 prototypeItem 后捌臊,可滾動(dòng)組件會(huì)在 layout 時(shí)計(jì)算一次它延主軸方向的長(zhǎng)度杨蛋,這樣也就預(yù)先知道了所有列表項(xiàng)的延主軸方向的長(zhǎng)度,所以和指定 itemExtent 一樣理澎,指定 prototypeItem 會(huì)有更好的性能。注意曙寡,itemExtent 和prototypeItem 互斥糠爬,不能同時(shí)指定它們。shrinkWrap
:該屬性表示是否根據(jù)子組件的總長(zhǎng)度來(lái)設(shè)置ListView的長(zhǎng)度举庶,默認(rèn)值為false 执隧。默認(rèn)情況下,ListView會(huì)在滾動(dòng)方向盡可能多的占用空間户侥。當(dāng)ListView在一個(gè)無(wú)邊界(滾動(dòng)方向上)的容器中時(shí)镀琉,shrinkWrap必須為true。addAutomaticKeepAlives
:該屬性我們將在介紹 PageView 組件時(shí)詳細(xì)解釋蕊唐。addRepaintBoundaries
:該屬性表示是否將列表項(xiàng)(子組件)包裹在RepaintBoundary組件中屋摔。RepaintBoundary 讀者可以先簡(jiǎn)單理解為它是一個(gè)”繪制邊界“,將列表項(xiàng)包裹在RepaintBoundary中可以避免列表項(xiàng)不必要的重繪替梨,但是當(dāng)列表項(xiàng)重繪的開(kāi)銷非常械鍪浴(如一個(gè)顏色塊,或者一個(gè)較短的文本)時(shí)副瀑,不添加RepaintBoundary反而會(huì)更高效(具體原因會(huì)在本書后面 Flutter 繪制原理相關(guān)章節(jié)中介紹)弓熏。如果列表項(xiàng)自身來(lái)維護(hù)是否需要添加繪制邊界組件,則此參數(shù)應(yīng)該指定為 false糠睡。
注意:上面這些參數(shù)并非
ListView
特有挽鞠,在本章后面介紹的其它可滾動(dòng)組件也可能會(huì)擁有這些參數(shù),它們的含義是相同的。
1. 默認(rèn)構(gòu)造函數(shù)
默認(rèn)構(gòu)造函數(shù)有一個(gè)children參數(shù)信认,它接受一個(gè)Widget列表(List<Widget>)串稀。這種方式適合只有少量的子組件數(shù)量已知且比較少的情況,反之則應(yīng)該使用ListView.builder 按需動(dòng)態(tài)構(gòu)建列表項(xiàng)
注意狮杨,雖然這種方式將所有children一次性傳遞給 ListView母截,但子組件仍然是在需要時(shí)才會(huì)加載(build(如有)、布局橄教、繪制)清寇,也就是說(shuō)通過(guò)默認(rèn)構(gòu)造函數(shù)構(gòu)建的 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("人的一切痛苦护蝶,本質(zhì)上都是對(duì)自己無(wú)能的憤怒华烟。", style: textStyle)),
Padding(
padding: EdgeInsets.all(8),
child: Text("人活在世界上,不可以有偏差持灰;而且多少要費(fèi)點(diǎn)勁兒盔夜,才能把自己保持到理性的軌道上。",
style: textStyle)),
Padding(
padding: EdgeInsets.all(8),
child: Text("我活在世上堤魁,無(wú)非想要明白些道理喂链,遇見(jiàn)些有趣的事。妥泉。", 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(
"聯(lián)系人 ${index + 1}",
style: TextStyle(color: Colors.orange, fontSize: 20),
),
subtitle: Text(
"聯(lián)系方式: 18826625555",
style: TextStyle(color: Colors.grey, fontSize: 16),
),
);
}),
);
}
}
2. ListView.builder
ListView.builder適合列表項(xiàng)比較多或者列表項(xiàng)不確定的情況椭微,下面看一下ListView.builder的核心參數(shù)列表
ListView.builder({
// ListView公共參數(shù)已省略
...
required IndexedWidgetBuilder itemBuilder,
int itemCount, // item數(shù)量
...
})
-
itemBuilder
:它是列表項(xiàng)的構(gòu)建器,類型為IndexedWidgetBuilder盲链,返回值為一個(gè)widget蝇率。當(dāng)列表滾動(dòng)到具體的index位置時(shí),會(huì)調(diào)用該構(gòu)建器構(gòu)建列表項(xiàng)刽沾。
*itemCount
:列表項(xiàng)的數(shù)量本慕,如果為null,則為無(wú)限列表侧漓。
示例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可以在生成的列表項(xiàng)之間添加一個(gè)分割組件锅尘,它比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割組件生成器
示例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(
"聯(lián)系人 ${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, // 左側(cè)間距
endIndent: 16, // 右側(cè)間距
);
},
itemCount: 100);
}
}
4. ListView.custom
我們看下ListView.custom的定義
const ListView.custom({
...
required this.childrenDelegate,
...
})
ListView.custom
主要是傳一個(gè)SliverChildDelegate
代理, SliverChildDelegate
是abstract(抽象類)鉴象,它有兩個(gè)子類SliverChildBuilderDelegate
、SliverChildListDelegate
** SliverChildListDelegate**
定義如下:
SliverChildListDelegate(
this.children, {
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
})
children
是必傳的何鸡,是不是很眼熟纺弊,ListView默認(rèn)構(gòu)造函數(shù)里也是傳一個(gè)children,實(shí)際上ListView默認(rèn)構(gòu)造函數(shù)中是通過(guò)children創(chuàng)建一個(gè)SliverChildListDelegate的
ListView構(gòu)造函數(shù)處理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
是必傳的骡男,是個(gè)NullableIndexedWidgetBuilder類型淆游,和IndexedWidgetBuilder類似,ListView.builder源碼中是通過(guò)builder創(chuàng)建個(gè)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. 固定高度列表
默認(rèn)情況下,列表中的Item的高度是隨內(nèi)容自適應(yīng)的犹菱。
但給列表指定 itemExtent 或 prototypeItem 會(huì)有更高的性能拾稳,所以當(dāng)我們知道列表項(xiàng)的高度都相同時(shí),強(qiáng)烈建議指定 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"),
);
},
);
}
}
自定義個(gè)LayoutLogPrint組件腊脱,在布局時(shí)可以打印當(dāng)前上下文中父組件給子組件的約束信息
完整代碼如下
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是一個(gè)自定義組件访得,在布局時(shí)可以打印當(dāng)前上下文中父組件給子組件的約束信息
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版本時(shí)會(huì)被去除
assert(() {
print('${tag ?? key ?? child}: $constraints');
return true;
}());
return child;
});
}
}
因?yàn)榱斜眄?xiàng)都是一個(gè) ListTile,高度相同陕凹,但是我們不知道 ListTile 的高度是多少悍抑,所以指定了prototypeItem ,運(yùn)行后杜耙,控制臺(tái)打铀崖狻:
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)
...
可見(jiàn) ListTile 的高度是 56 ,指定itemExtent為56也是可以的,建議優(yōu)先指定原型佑女,這樣的話在列表項(xiàng)布局修改后记靡,仍然可以正常工作(前提是每個(gè)列表項(xiàng)的高度相同)
如果本例中不指定 itemExtent 或 prototypeItem ,我們看看控制臺(tái)日志信息
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)
...
可以發(fā)現(xiàn)团驱,列表不知道列表項(xiàng)的具體高度摸吠,高度約束變?yōu)?0.0 到 Infinity。
6.列表的原理
ListView 內(nèi)部組合了 Scrollable店茶、Viewport 和 Sliver蜕便,需要注意:
- ListView 中的列表項(xiàng)組件都是 RenderBox,并不是 Sliver贩幻, 這個(gè)一定要注意。
- 一個(gè) ListView 中只有一個(gè)Sliver两嘴,對(duì)列表項(xiàng)進(jìn)行按需加載的邏輯是 Sliver 中實(shí)現(xiàn)的丛楚。
- ListView 的 Sliver 默認(rèn)是 SliverList,如果指定了
itemExtent
憔辫,則會(huì)使用 SliverFixedExtentList趣些;如果prototypeItem
屬性不為空,則會(huì)使用 SliverPrototypeExtentList贰您,無(wú)論是是哪個(gè)坏平,都實(shí)現(xiàn)了子組件的按需加載模型。
- ListView 的 Sliver 默認(rèn)是 SliverList,如果指定了
我們解釋下ListView的Sliver
ListView 是繼承于BoxScrollView锦亦,BoxScrollView繼承于ScrollView舶替,ScrollView繼承于StatelessWidget,因而ScrollView是我們需要讀的最深層級(jí)杠园,在ScrollView的方法中顾瞪,是通過(guò)buildSlivers來(lái)獲取slivers的,而buildSlivers方法在ScrollView中是抽象方法,因而它的子類需要實(shí)現(xiàn)〕滦眩現(xiàn)在我們需要看下BoxScrollView中關(guān)于buildSlivers的實(shí)現(xiàn)惕橙,在BoxScrollView的 buildSlivers方法中是通過(guò)buildChildLayout來(lái)獲取Sliver,而buildChildLayout在BoxScrollView中也是抽象方法钉跷,因而我們?nèi)タ碆oxScrollView子類ListView中buildChildLayout的實(shí)現(xiàn)弥鹦,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);
}
默認(rèn)情況下 ListView的Sliver是SliverList,在itemExtent不為空時(shí)爷辙,是SliverFixedExtentList彬坏,在prototypeItem不為空時(shí)是SliverPrototypeExtentList
7 實(shí)例:無(wú)限加載列表
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();
// 加載時(shí)顯示loading
return Container(
padding: EdgeInsets.all(16),
alignment: Alignment.center,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2.0),
),
);
} else {
// 已經(jīng)加載了100條數(shù)據(jù),不再獲取數(shù)據(jù)犬钢。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
child: Text("沒(méi)有更多", 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對(duì)單詞
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);
});
}
}