目錄
1. ListView
2. SingleChildScrollView
3. GridView(二維網(wǎng)格列表)
Flutter官方并沒(méi)有對(duì)Widget進(jìn)行分類(lèi),對(duì)其分類(lèi)主要是為了對(duì)Widget進(jìn)行功能區(qū)分。
當(dāng)組件超過(guò)顯示窗口時(shí),F(xiàn)lutter會(huì)提示Overflow錯(cuò)誤。為此,F(xiàn)lutter提供了多種可滾動(dòng)組件用于顯示列表和長(zhǎng)布局。
/*
Flutter有兩種布局模型:
1. 基于RenderBox的盒模型布局。
2. 基于RenderSliver (Sliver) 按需加載列表布局。
*/
主縱軸
滾動(dòng)方向稱(chēng)為主軸,非滾動(dòng)方向稱(chēng)為縱軸。
可滾動(dòng)組件的組成部分
1. Scrollable (繼承自StatefulWidget)
處理滑動(dòng)手勢(shì),確定滑動(dòng)偏移,滑動(dòng)偏移變化時(shí)構(gòu)建Viewport 。
2. Viewport
渲染當(dāng)前視口中需要顯示的Sliver。
父組件為Scrollable。
3. Sliver
對(duì)子組件進(jìn)行構(gòu)建和布局。
父組件為Viewport。
具體過(guò)程
1. Scrollable監(jiān)聽(tīng)到用戶(hù)滑動(dòng)行為后,根據(jù)最新的滑動(dòng)偏移構(gòu)建Viewport 。
2. Viewport將當(dāng)前視口信息和配置信息通過(guò)SliverConstraints傳遞給Sliver。
3. Sliver中對(duì)子組件(RenderBox)按需進(jìn)行構(gòu)建和布局,然后確認(rèn)自身的位置、繪制等信息,保存在geometry(SliverGeometry類(lèi)型的對(duì)象)中。
基于Sliver的延遲構(gòu)建模型
通常可滾動(dòng)組件的子組件非常多、占用高度非常大,如果一次性將子組件全部構(gòu)建將會(huì)非常昂貴。為此,F(xiàn)lutter提出Sliver(薄片)概念,如果一個(gè)可滾動(dòng)組件支持Sliver模型,則可以分成許多Sliver,且只有當(dāng)Sliver出現(xiàn)在視口中時(shí)才去構(gòu)建它。
支持:ListView、GridView。不支持:SingleChildScrollView。
公共屬性(最終會(huì)透?jìng)鹘oScrollable和Viewport)
1. scrollDirection
滾動(dòng)方向。
Axis.vertica垂直方向(默認(rèn)),Axis.horizontal水平方向。
2. reverse
是否按照閱讀方向相反的方向滑動(dòng)。
決定可滾動(dòng)組件的初始滾動(dòng)位置是在“頭”還是“尾”,取false時(shí)初始滾動(dòng)位置在“頭”,反之則在“尾”。
3. primary
是否使用widget樹(shù)中默認(rèn)的PrimaryScrollController;
當(dāng)滑動(dòng)方向?yàn)榇怪狈较蚯覜](méi)有指定controller時(shí),primary默認(rèn)為true。
4. padding
內(nèi)邊距
5. controller(ScrollController類(lèi)型)
控制滾動(dòng)位置和監(jiān)聽(tīng)滾動(dòng)事件。
當(dāng)子樹(shù)中的可滾動(dòng)組件沒(méi)有顯式指定controller且primary屬性值為true時(shí)(默認(rèn)就為true),可滾動(dòng)組件會(huì)使用Widget樹(shù)中默認(rèn)的PrimaryScrollController。這種機(jī)制的好處是父組件可以控制子樹(shù)中可滾動(dòng)組件的滾動(dòng)行為,例如,Scaffold正是使用這種機(jī)制在iOS中實(shí)現(xiàn)了點(diǎn)擊導(dǎo)航欄回到頂部的功能。
6. physics(ScrollPhysics類(lèi)型)
決定可滾動(dòng)組件如何響應(yīng)用戶(hù)操作。比如用戶(hù)滑動(dòng)完抬起手指后,繼續(xù)執(zhí)行動(dòng)畫(huà);或者滑動(dòng)到邊界時(shí),如何顯示。NeverScrollableScrollPhysics():禁止?jié)L動(dòng)。
Flutter默認(rèn)會(huì)根據(jù)各平臺(tái)分別使用不同的ScrollPhysics對(duì)象,應(yīng)用不同的顯示效果,如當(dāng)滑動(dòng)到邊界時(shí),繼續(xù)拖動(dòng)的話,在iOS上會(huì)出現(xiàn)彈性效果,而在Android上會(huì)出現(xiàn)微光效果。如果想在所有平臺(tái)下使用同一種效果,可以顯式指定:
1. ClampingScrollPhysics:Android下微光效果。
2. BouncingScrollPhysics:iOS下彈性效果。
cacheExtent
預(yù)渲染的高度(下圖中頂部和底部灰色的區(qū)域)。
如果RenderBox進(jìn)入這個(gè)區(qū)域,即使它未顯示在屏幕上,也要先進(jìn)行構(gòu)建,預(yù)渲染是為了后面進(jìn)入Viewport時(shí)更流暢。
默認(rèn)值是250,在構(gòu)建可滾動(dòng)列表時(shí)可以指定這個(gè)值(最終會(huì)傳給 Viewport)。

- Scrollable、Viewport、Sliver
Scrollable({
this.axisDirection = AxisDirection.down, // 滾動(dòng)方向
this.controller,
this.physics,
// 滑動(dòng)時(shí)Scrollable會(huì)調(diào)用此回調(diào)構(gòu)建新的Viewport,同時(shí)傳遞一個(gè)ViewportOffset類(lèi)型的offset參數(shù)(描述Viewport該顯示哪一部分)。
// 重新構(gòu)建Viewport(本身也是Widget,只是配置信息)不是一個(gè)昂貴的操作,Viewport變化時(shí)對(duì)應(yīng)的RenderViewport會(huì)更新信息,并不會(huì)隨著Widget進(jìn)行重新構(gòu)建。
@required this.viewportBuilder,
})
Viewport({
Key? key,
this.axisDirection = AxisDirection.down,
this.crossAxisDirection,
this.anchor = 0.0,
// 滾動(dòng)偏移。Scrollabel構(gòu)建Viewport 時(shí)傳入(描述了Viewport該顯示哪一部分)。
required ViewportOffset offset,
// 類(lèi)型為Key,表示從什么地方開(kāi)始繪制,默認(rèn)是第一個(gè)元素
this.center,
this.cacheExtent, // 預(yù)渲染區(qū)域
// pixel:cacheExtent值為預(yù)渲染區(qū)域的具體像素長(zhǎng)度
// viewport:cacheExtent值是一個(gè)乘數(shù),預(yù)渲染區(qū)域的像素長(zhǎng)度=cacheExtent*viewport。
this.cacheExtentStyle = CacheExtentStyle.pixel,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[], // 需要顯示的 Sliver 列表
})
Sliver對(duì)應(yīng)的渲染對(duì)象類(lèi)型是RenderSliver。
RenderSliver和RenderBox的相同點(diǎn)是都繼承自RenderObject類(lèi),不同點(diǎn)是在布局時(shí)約束信息不同。RenderBox在布局時(shí)父組件傳遞給它的約束信息是BoxConstraints(最大最小寬高約束);而 RenderSliver在布局時(shí)父組件傳遞給它的約束是SliverConstraints。
- Scrollbar (Material風(fēng)格的滾動(dòng)條)
使用:作為可滾動(dòng)組件的任意一個(gè)父組件即可。
Scrollbar(
child: SingleChildScrollView(
...
),
);
Scrollbar在iOS平臺(tái)會(huì)自動(dòng)切換為CupertinoScrollbar(iOS風(fēng)格)。
Scrollbar和CupertinoScrollbar都是通過(guò)監(jiān)聽(tīng)滾動(dòng)通知來(lái)確定滾動(dòng)條位置的。
- ScrollController(間接繼承自Listenable)
可滾動(dòng)組件都有一個(gè)controller屬性(控制和監(jiān)聽(tīng)滾動(dòng))
ScrollController({
double initialScrollOffset = 0.0, // 初始滾動(dòng)位置
this.keepScrollOffset = true,// 是否保存滾動(dòng)位置
...
})
監(jiān)聽(tīng)滾動(dòng)事件
controller.addListener(()=>print(controller.offset))
常用的屬性和方法:
1. offset
可滾動(dòng)組件當(dāng)前的滾動(dòng)位置。
2. jumpTo(double offset)、animateTo(double offset,...)
用于跳轉(zhuǎn)到指定的位置,不同之處在于,后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫(huà)。
示例
創(chuàng)建一個(gè)ListView,判斷當(dāng)前位置是否超過(guò)1000像素,如果超過(guò)則在屏幕右下角顯示一個(gè)“返回頂部”的按鈕,該按鈕點(diǎn)擊后可以使ListView恢復(fù)到初始位置;如果沒(méi)有超過(guò)1000像素,則隱藏“返回頂部”按鈕。
class ScrollControllerTestRoute extends StatefulWidget {
@override
ScrollControllerTestRouteState createState() {
return new ScrollControllerTestRouteState();
}
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
ScrollController _controller = new ScrollController();
bool showToTopBtn = false; // 是否顯示“返回到頂部”按鈕
@override
void initState() {
super.initState();
// 監(jiān)聽(tīng)滾動(dòng)事件,打印滾動(dòng)位置
_controller.addListener(() {
print(_controller.offset); //打印滾動(dòng)位置
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
// 為了避免內(nèi)存泄露,需要調(diào)用_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("滾動(dòng)控制")),
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0, // 列表項(xiàng)高度固定時(shí),顯式指定高度是一個(gè)好習(xí)慣(性能消耗小)
controller: _controller,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到頂部時(shí)執(zhí)行動(dòng)畫(huà)
_controller.animateTo(.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease
);
}
),
);
}
}


滾動(dòng)位置恢復(fù)
PageStorage是一個(gè)用于保存頁(yè)面(路由)相關(guān)數(shù)據(jù)的功能型組件,它擁有一個(gè)存儲(chǔ)桶,子樹(shù)中的Widget可以通過(guò)指定不同的PageStorageKey來(lái)存儲(chǔ)各自的數(shù)據(jù)或狀態(tài)。
每次滾動(dòng)結(jié)束,可滾動(dòng)組件都會(huì)將滾動(dòng)位置offset存儲(chǔ)到PageStorage中,當(dāng)可滾動(dòng)組件重新創(chuàng)建時(shí)再恢復(fù)。
ScrollController.keepScrollOffset為false,則滾動(dòng)位置將不會(huì)被存儲(chǔ),可滾動(dòng)組件重新創(chuàng)建時(shí)會(huì)使用ScrollController.initialScrollOffset;
ScrollController.keepScrollOffset為true時(shí),可滾動(dòng)組件在第一次創(chuàng)建時(shí),會(huì)滾動(dòng)到initialScrollOffset處,因?yàn)檫@時(shí)還沒(méi)有存儲(chǔ)過(guò)滾動(dòng)位置。在接下來(lái)的滾動(dòng)中就會(huì)存儲(chǔ)、恢復(fù)滾動(dòng)位置,忽略initialScrollOffset。
當(dāng)一個(gè)路由中包含多個(gè)可滾動(dòng)組件時(shí),如果發(fā)現(xiàn)在進(jìn)行一些跳轉(zhuǎn)或切換操作后,滾動(dòng)位置不能正確恢復(fù),這時(shí)可以通過(guò)顯式指定不同的PageStorageKey來(lái)分別跟蹤不同的可滾動(dòng)組件的位置,如:
ListView(key: PageStorageKey(1), ... );
ListView(key: PageStorageKey(2), ... );
注意:一個(gè)路由中包含多個(gè)可滾動(dòng)組件時(shí),如果要分別跟蹤它們的滾動(dòng)位置,并非一定就得給他們分別提供PageStorageKey。這是因?yàn)镾crollable本身是一個(gè)StatefulWidget,它的狀態(tài)中也會(huì)保存當(dāng)前滾動(dòng)位置,所以,只要可滾動(dòng)組件本身沒(méi)有被從樹(shù)上detach掉,那么其State就不會(huì)銷(xiāo)毀,滾動(dòng)位置就不會(huì)丟失。只有當(dāng)Widget發(fā)生結(jié)構(gòu)變化,導(dǎo)致可滾動(dòng)組件的State銷(xiāo)毀或重新構(gòu)建時(shí)才會(huì)丟失狀態(tài),這種情況就需要顯式指定PageStorageKey,通過(guò)PageStorage來(lái)存儲(chǔ)滾動(dòng)位置,一個(gè)典型的場(chǎng)景是在使用TabBarView時(shí),在Tab發(fā)生切換時(shí),Tab頁(yè)中的可滾動(dòng)組件的State就會(huì)銷(xiāo)毀,這時(shí)如果想恢復(fù)滾動(dòng)位置就需要指定PageStorageKey。
ScrollPosition
真正保存滑動(dòng)位置信息的對(duì)象。
offset只是一個(gè)便捷屬性:double get offset => position.pixels;
一個(gè)ScrollController對(duì)象可以同時(shí)被多個(gè)可滾動(dòng)組件使用,ScrollController會(huì)為每一個(gè)可滾動(dòng)組件創(chuàng)建一個(gè)ScrollPosition對(duì)象,并保存在positions屬性中(List<ScrollPosition>)。
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
controller.positions.length 被幾個(gè)可滾動(dòng)組件使用
ScrollController的animateTo() 和 jumpTo(),內(nèi)部最終都會(huì)調(diào)用ScrollPosition的同名方法(真正來(lái)控制跳轉(zhuǎn)滾動(dòng)位置)。
ScrollController控制原理
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
當(dāng)ScrollController和可滾動(dòng)組件關(guān)聯(lián)時(shí),可滾動(dòng)組件首先會(huì)調(diào)用ScrollController的createScrollPosition()方法來(lái)創(chuàng)建一個(gè)ScrollPosition來(lái)存儲(chǔ)滾動(dòng)位置信息,接著,可滾動(dòng)組件會(huì)調(diào)用attach()方法,將創(chuàng)建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱(chēng)為“注冊(cè)位置”,只有注冊(cè)后animateTo() 和 jumpTo()才可以被調(diào)用。
當(dāng)可滾動(dòng)組件銷(xiāo)毀時(shí),會(huì)調(diào)用ScrollController的detach()方法,將其ScrollPosition對(duì)象從ScrollController的positions屬性中移除,這一步稱(chēng)為“注銷(xiāo)位置”,注銷(xiāo)后animateTo() 和 jumpTo() 將不能再被調(diào)用。
注意:ScrollController的animateTo() 和 jumpTo()內(nèi)部會(huì)調(diào)用【所有】ScrollPosition的同名方法。
滾動(dòng)監(jiān)聽(tīng)
Flutter Widget樹(shù)中子Widget可以通過(guò)發(fā)送通知(Notification)與父(包括祖先)Widget通信。父級(jí)組件可以通過(guò)NotificationListener組件來(lái)監(jiān)聽(tīng)自己關(guān)注的通知,這種通信方式類(lèi)似于Web開(kāi)發(fā)中瀏覽器的事件冒泡。
可滾動(dòng)組件在滾動(dòng)時(shí)會(huì)發(fā)送ScrollNotification類(lèi)型的通知,ScrollBar正是通過(guò)監(jiān)聽(tīng)滾動(dòng)通知來(lái)實(shí)現(xiàn)的。通過(guò)NotificationListener監(jiān)聽(tīng)滾動(dòng)事件和通過(guò)ScrollController有兩個(gè)主要的不同:
1. 通過(guò)NotificationListener可以在從可滾動(dòng)組件到widget樹(shù)根之間任意位置都能監(jiān)聽(tīng)。而ScrollController只能和具體的可滾動(dòng)組件關(guān)聯(lián)后才可以。
2. 收到滾動(dòng)事件后獲得的信息不同;NotificationListener在收到滾動(dòng)事件時(shí),通知中會(huì)攜帶當(dāng)前滾動(dòng)位置和ViewPort的一些信息,而ScrollController只能獲取當(dāng)前滾動(dòng)位置。
在接收到滾動(dòng)事件時(shí),參數(shù)類(lèi)型為ScrollNotification,它包括一個(gè)metrics屬性,它的類(lèi)型是ScrollMetrics,該屬性包含當(dāng)前ViewPort及滾動(dòng)位置等信息:
1. pixels:當(dāng)前滾動(dòng)位置。
2. maxScrollExtent:最大可滾動(dòng)長(zhǎng)度。
3. extentBefore:滑出ViewPort頂部的長(zhǎng)度;此示例中相當(dāng)于頂部滑出屏幕上方的列表長(zhǎng)度。
4. extentInside:ViewPort內(nèi)部長(zhǎng)度;此示例中屏幕顯示的列表部分的長(zhǎng)度。
5. extentAfter:列表中未滑入ViewPort部分的長(zhǎng)度;此示例中列表底部未顯示到屏幕范圍部分的長(zhǎng)度。
6. atEdge:是否滑到了可滾動(dòng)組件的邊界。
示例
import 'package:flutter/material.dart';
class ScrollNotificationTestRoute extends StatefulWidget {
@override
_ScrollNotificationTestRouteState createState() =>
new _ScrollNotificationTestRouteState();
}
class _ScrollNotificationTestRouteState
extends State<ScrollNotificationTestRoute> {
String _progress = "0%"; // 保存進(jìn)度百分比
@override
Widget build(BuildContext context) {
return Scrollbar( // 進(jìn)度條
// 監(jiān)聽(tīng)滾動(dòng)通知
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
// 重新構(gòu)建
setState(() {
_progress = "${(progress * 100).toInt()}%";
});
print("BottomEdge: ${notification.metrics.extentAfter == 0}");
//return true; // 放開(kāi)此行注釋后,進(jìn)度條將失效
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"));
}
),
CircleAvatar( //顯示進(jìn)度百分比
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
);
}
}

1. ListView (建議指定itemExtent或prototypeItem)
沿一個(gè)方向線性排列所有子組件。支持基于Sliver的延遲構(gòu)建模型。
/*
1. ListView中的列表項(xiàng)組件是RenderBox,并不是Sliver。
2. 一個(gè)ListView中只有一個(gè)Sliver(對(duì)列表項(xiàng)進(jìn)行按需加載),默認(rèn)是SliverList,如果指定了itemExtent,則為SliverFixedExtentList;如果prototypeItem屬性不為空,則為SliverPrototypeExtentList。
3. 可以通過(guò)ListView.custom自定義列表項(xiàng)生成模型,它需要實(shí)現(xiàn)一個(gè)SliverChildDelegate用來(lái)給ListView生成列表項(xiàng)組件。
4. 可滾動(dòng)組件的構(gòu)造函數(shù)如果需要一個(gè)列表項(xiàng)Builder則支持基于Sliver的懶加載模型的,反之則不支持。
5. ListView高度邊界無(wú)法確定時(shí)會(huì)異常
*/
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,
// 子widget列表
// 這種方式適合只有少量的子組件數(shù)量已知且比較少的情況,反之則應(yīng)該使用ListView.builder 按需動(dòng)態(tài)構(gòu)建列表項(xiàng)。
List<Widget> children = const <Widget>[],
})
說(shuō)明:
1. itemExtent
如果不為null,則表示滾動(dòng)方向上子組件的長(zhǎng)度。
指定后滾動(dòng)系統(tǒng)可以提前知道列表的長(zhǎng)度,而無(wú)需每次構(gòu)建子組件時(shí)都去再計(jì)算,會(huì)更加高效。
2. prototypeItem(列表項(xiàng)原型)
如果所有列表項(xiàng)長(zhǎng)度相同但不知道具體多少,可以指定一個(gè)列表項(xiàng)prototypeItem,可滾動(dòng)組件會(huì)在layout時(shí)計(jì)算一次它延主軸方向的長(zhǎng)度,和指定itemExtent一樣。
注意:itemExtent和prototypeItem互斥,不能同時(shí)指定。
3. shrinkWrap
是否根據(jù)子組件的總長(zhǎng)度來(lái)設(shè)置ListView的長(zhǎng)度。
默認(rèn)false ,ListView的會(huì)在滾動(dòng)方向盡可能多的占用空間。當(dāng)ListView在一個(gè)無(wú)邊界(滾動(dòng)方向上)的容器中時(shí),shrinkWrap必須為true。
4. addAutomaticKeepAlives
是否將列表項(xiàng)(子組件)包裹在AutomaticKeepAlive 組件中;
如果設(shè)置為true(默認(rèn)為true,在懶加載列表中會(huì)為每一個(gè)列表項(xiàng)添加AutomaticKeepAlive父組件),在列表項(xiàng)滑出視口時(shí)不會(huì)被GC(垃圾回收),它會(huì)使用KeepAliveNotification來(lái)保存其狀態(tài)。
如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false。
5. addRepaintBoundaries
是否將列表項(xiàng)(子組件)包裹在RepaintBoundary組件中。默認(rèn)為true。
將列表項(xiàng)包裹在RepaintBoundary中可以在滾動(dòng)時(shí)避免列表項(xiàng)重繪,但是當(dāng)列表項(xiàng)重繪的開(kāi)銷(xiāo)非常小時(shí),不添加RepaintBoundary反而會(huì)更高效。
如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false
示例
ListView(
children: [
imgSection,
titleSection,
buttonSection,
textSection,
],
),
ListView(
padding: EdgeInsets.all(10),
children: [
ListTitle(
title:Text('hello'),
subTitle:('world'),
),
ListTitle(
leading:Icon(Icons.settings,color:Colors.yellow,size:30),
trailing:Image.network("http://.../1.png"),
title:Text(
'hello',
style: TextStyle(
fontSize: 24,
),
),
subTitle:('world'),
),
],
),
- 默認(rèn)構(gòu)造函數(shù)
有一個(gè)children參數(shù),子組件很少時(shí)使用。不支持基于Sliver的懶加載模型。
通過(guò)此方式創(chuàng)建的ListView和使用SingleChildScrollView+Column的方式?jīng)]有本質(zhì)的區(qū)別。
示例
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'),
],
);
示例2
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Basic List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView(
children: <Widget>[
new ListTile(
leading: new Icon(Icons.map),
title: new Text('Map'),
),
new ListTile(
leading: new Icon(Icons.photo),
title: new Text('Album'),
),
new ListTile(
leading: new Icon(Icons.phone),
title: new Text('Phone'),
),
],
),
),
);
}
}
示例3(水平滾動(dòng))
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Horizontal List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Container(
margin: new EdgeInsets.symmetric(vertical: 20.0),
height: 200.0,
child: new ListView(
scrollDirection: Axis.horizontal, // 水平滾動(dòng)
children: <Widget>[
new Container(
width: 260.0,
color: Colors.red,
),
new Container(
width: 260.0,
color: Colors.blue,
),
],
),
),
),
);
}
}
- ListView.builder 構(gòu)造函數(shù)
適合列表項(xiàng)比較多或不確定時(shí)。支持基于Sliver的懶加載模型的。
ListView.builder({
...
// 列表項(xiàng)的構(gòu)建器,返回值為一個(gè)widget。當(dāng)滾動(dòng)到對(duì)應(yīng)index位置時(shí)會(huì)調(diào)用。
@required IndexedWidgetBuilder itemBuilder,
// 列表項(xiàng)的數(shù)量,如果為null,則為無(wú)限列表。
int itemCount,
})
示例
ListView.builder(
itemCount: 100,
itemExtent: 50.0, // 高度為50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);

示例2
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp(
items: new List<String>.generate(10000, (i) => "Item $i"),
));
}
class MyApp extends StatelessWidget {
final List<String> items; // 數(shù)據(jù)源
MyApp({Key key, @required this.items}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = 'Long List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('${items[index]}'),
);
},
),
),
);
}
}
示例3(不同類(lèi)型的item)
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp(
items: new List<ListItem>.generate(
1000,
(i) => i % 6 == 0
? new HeadingItem("Heading $i")
: new MessageItem("Sender $i", "Message body $i"),
),
));
}
class MyApp extends StatelessWidget {
final List<ListItem> items;
MyApp({Key key, @required this.items}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = 'Mixed List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is HeadingItem) {
return new ListTile(
title: new Text(
item.heading,
style: Theme.of(context).textTheme.headline,
),
);
} else if (item is MessageItem) {
return new ListTile(
title: new Text(item.sender),
subtitle: new Text(item.body),
);
}
},
),
),
);
}
}
abstract class ListItem {}
class HeadingItem implements ListItem {
final String heading;
HeadingItem(this.heading);
}
class MessageItem implements ListItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
}
- ListView.separated
比ListView.builder多了一個(gè)separatorBuilder參數(shù)(在生成的列表項(xiàng)之間添加分割組件)。
示例(奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條綠色下劃線)
class ListView3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
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) { // 分割器構(gòu)造器
return index%2==0?divider1:divider2;
},
);
}
}

示例2
從數(shù)據(jù)源異步分批拉取一些數(shù)據(jù),然后用ListView展示,當(dāng)滑動(dòng)到列表末尾時(shí),判斷是否需要再去拉取數(shù)據(jù),如果是,則去拉取,拉取過(guò)程中在表尾顯示一個(gè)loading,拉取成功后將數(shù)據(jù)插入列表;如果不需要再去拉取,則在表尾提示"沒(méi)有更多"
class InfiniteListView extends StatefulWidget {
@override
_InfiniteListViewState createState() => new _InfiniteListViewState();
}
class _InfiniteListViewState extends State<InfiniteListView> {
static const loadingTag = "##loading##"; //表尾標(biāo)記
var _words = <String>[loadingTag];
@override
void initState() {
super.initState();
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: _words.length,
itemBuilder: (context, index) {
// 如果到了表尾
if (_words[index] == loadingTag) {
//不足100條,繼續(xù)獲取數(shù)據(jù)
if (_words.length - 1 < 100) {
// 獲取數(shù)據(jù)
_retrieveData();
// 加載時(shí)顯示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 {
// 已經(jīng)加載了100條數(shù)據(jù),不再獲取數(shù)據(jù)。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: Text("沒(méi)有更多了", style: TextStyle(color: Colors.grey),)
);
}
}
// 顯示單詞列表項(xiàng)
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (context, index) => Divider(height: .0),
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
setState(() {
// 重新構(gòu)建列表
_words.insertAll(_words.length - 1,
// 每次生成20個(gè)單詞
generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
);
});
});
}
}


- 添加固定的列表頭
不太好的寫(xiě)法:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
SizedBox(
// Material設(shè)計(jì)規(guī)范中狀態(tài)欄、導(dǎo)航欄、ListTile高度分別為24、56、56 。避免底部留白
height: MediaQuery.of(context).size.height-24-56-56,
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
)
]);
}
這種方法太不好,如果頁(yè)面布局發(fā)生變化,比如表頭布局調(diào)整導(dǎo)致表頭高度改變,那么剩余空間的高度就得重新計(jì)算。修正:
// 自動(dòng)拉伸ListView以填充屏幕剩余空間
@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"));
}),
),
]);
}

- AutomaticKeepAlive組件
將列表項(xiàng)的根RenderObject的keepAlive按需自動(dòng)標(biāo)記為true或false。
列表組件的Viewport區(qū)域+cacheExtent預(yù)渲染區(qū)域 稱(chēng)為加載區(qū)域 :
1. 當(dāng) keepAlive 標(biāo)記為 false 時(shí),如果列表項(xiàng)滑出加載區(qū)域時(shí),列表組件將會(huì)被銷(xiāo)毀。
2. 當(dāng) keepAlive 標(biāo)記為 true 時(shí),當(dāng)列表項(xiàng)滑出加載區(qū)域后,Viewport 會(huì)將列表組件緩存起來(lái);當(dāng)列表項(xiàng)進(jìn)入加載區(qū)域時(shí),Viewport 從先從緩存中查找是否已經(jīng)緩存,如果有則直接復(fù)用,如果沒(méi)有則重新創(chuàng)建列表項(xiàng)。
子組件想改變是否需要緩存的狀態(tài)時(shí)就向KeepAliveNotification通知,AutomaticKeepAlive收到消息后會(huì)去更改keepAlive的狀態(tài)(從true變?yōu)閒alse時(shí),需要釋放緩存)。
- 優(yōu)化ListView
1.
列表項(xiàng)較多或不確定(上拉加載更多)時(shí)不要使用默認(rèn)的構(gòu)造函數(shù),應(yīng)該使用ListView.builder
2.
禁用addAutomaticKeepAlives(缺點(diǎn):滑動(dòng)過(guò)快時(shí)可能會(huì)出現(xiàn)短暫白屏)。
禁用addRepaintBoundaries,當(dāng)列表元素布局較簡(jiǎn)單時(shí)可提高流暢度。
3.
列表中不可變子組件使用const修飾。 // children: [const ListImage()],
4.
指定itemExtent值(當(dāng)可以提前知道時(shí))。
- AnimatedList(在列表中插入或刪除節(jié)點(diǎn)時(shí)執(zhí)行一個(gè)動(dòng)畫(huà))
AnimatedList(StatefulWidget類(lèi)型)對(duì)應(yīng)的State類(lèi)型為AnimatedListState(包含了添加和刪除元素的方法):
void insertItem(int index, { Duration duration = _kDuration });
void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) ;
要使用上面的添加和刪除方法則需要?jiǎng)?chuàng)建GlobalKey并賦值給AnimatedList的key,通過(guò)key.currentState獲取到AnimatedListState對(duì)象來(lái)調(diào)用。
final globalKey = GlobalKey<AnimatedListState>();
AnimatedList(key: globalKey, ...)
globalKey.currentState.insertItem(data.length - 1);
在插入和刪除數(shù)據(jù)時(shí),應(yīng)該是先修改列表數(shù)據(jù),然后調(diào)用 AnimatedListState 的insertItem 和 removeItem 方法,而不能直接操作完數(shù)據(jù)后刷新界面。
示例(AnimatedList)
點(diǎn)擊底部 + 按鈕時(shí)向列表追加一個(gè)列表項(xiàng);點(diǎn)擊每個(gè)列表項(xiàng)后面的刪除按鈕時(shí),刪除該列表項(xiàng),添加和刪除時(shí)分別執(zhí)行指定的動(dòng)畫(huà)(漸顯、漸隱+收縮)。
class AnimatedListRoute extends StatefulWidget {
const AnimatedListRoute({Key? key}) : super(key: key);
@override
_AnimatedListRouteState createState() => _AnimatedListRouteState();
}
class _AnimatedListRouteState extends State<AnimatedListRoute> {
var data = <String>[];
int counter = 5;
final globalKey = GlobalKey<AnimatedListState>();
@override
void initState() {
for (var i = 0; i < counter; i++) {
data.add('${i + 1}');
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// 與ListView的itemBuilder相比多了一個(gè)animation參數(shù)
// typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index,Animation<double> animation);
AnimatedList(
key: globalKey,
initialItemCount: data.length,
itemBuilder: (
BuildContext context,
int index,
Animation<double> animation,
) {
// 添加列表項(xiàng)時(shí)會(huì)執(zhí)行漸顯動(dòng)畫(huà)
return FadeTransition(
opacity: animation,
child: buildItem(context, index),
);
},
),
buildAddBtn(),
],
);
}
// 創(chuàng)建一個(gè) “+” 按鈕,點(diǎn)擊后會(huì)向列表中插入一項(xiàng)
Widget buildAddBtn() {
return Positioned(
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 添加一個(gè)列表項(xiàng)
data.add('${++counter}');
// 告訴列表項(xiàng)有新添加的列表項(xiàng)
globalKey.currentState!.insertItem(data.length - 1);
print('添加 $counter');
},
),
bottom: 30,
left: 0,
right: 0,
);
}
// 構(gòu)建列表項(xiàng)
Widget buildItem(context, index) {
String char = data[index];
return ListTile(
// 數(shù)字不會(huì)重復(fù),所以作為Key
key: ValueKey(char),
title: Text(char),
trailing: IconButton(
icon: Icon(Icons.delete),
// 點(diǎn)擊時(shí)刪除
onPressed: () => onDelete(context, index),
),
);
}
void onDelete(context, index) {
setState(() {
globalKey.currentState!.removeItem(
index,
(context, animation) {
// 刪除過(guò)程執(zhí)行的是反向動(dòng)畫(huà),animation.value 會(huì)從1變?yōu)?
var item = buildItem(context, index);
print('刪除 ${data[index]}');
data.removeAt(index);
// 刪除動(dòng)畫(huà)是一個(gè)合成動(dòng)畫(huà):漸隱 + 縮小列表項(xiàng)告訴
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
// 讓透明度變化的更快一些
curve: const Interval(0.5, 1.0),
),
// 不斷縮小列表項(xiàng)的高度
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: item,
),
);
},
duration: Duration(milliseconds: 200), // 動(dòng)畫(huà)時(shí)間為 200 ms
);
});
}
}

示例
AnimatedList顯示與ListModel保持同步的卡片列表。當(dāng)新的item被添加到ListModel或從ListModel中刪除時(shí),相應(yīng)的卡片在UI上也會(huì)被添加或刪除,并伴有動(dòng)畫(huà)效果。
點(diǎn)擊一個(gè)item選擇它,再次點(diǎn)擊它會(huì)取消選擇。點(diǎn)擊’+’插入選定的item,點(diǎn)擊’ - ‘刪除選定的item。 tap處理器會(huì)從ListModel<E>中添加或刪除items,ListModel<E>是List<E>的簡(jiǎn)單封裝 ,用于保持和AnimatedList的同步。 列表模型為其動(dòng)畫(huà)列表提供了一個(gè)GlobalKey。它使用該鍵來(lái)調(diào)用由AnimatedListState定義的insertItem和removeItem方法。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AnimatedListSample extends StatefulWidget {
@override
_AnimatedListSampleState createState() => new _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
// 由于AnimatedList的所有控制都是在AnimatedState中進(jìn)行的。在構(gòu)建AnimatedList時(shí)給key屬性賦值GlobalKey,就可以通過(guò)key.currentState獲取到AnimatedListState對(duì)象。
final GlobalKey<AnimatedListState> _listKey = new GlobalKey<AnimatedListState>();
ListModel<int> _list;
int _selectedItem;
int _nextItem;
@override
void initState() {
super.initState();
_list = new ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
// 構(gòu)建列表項(xiàng)(沒(méi)有被移除的)
Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
return new CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
//
Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
return new CardItem(
animation: animation,
item: item,
selected: false,
);
}
// 插入
void _insert() {
final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
_list.insert(index, _nextItem++);
}
// 移除選中
void _remove() {
if (_selectedItem != null) {
_list.removeAt(_list.indexOf(_selectedItem));
setState(() {
_selectedItem = null;
});
}
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.add_circle),
onPressed: _insert,
tooltip: 'insert a new item',
),
new IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: _remove,
tooltip: 'remove the selected item',
),
],
),
body: new Padding(
padding: const EdgeInsets.all(16.0),
child: new AnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
),
);
}
}
//
class ListModel<E> {
ListModel({
@required this.listKey,
@required this.removedItemBuilder,
Iterable<E> initialItems,
}) : assert(listKey != null),
assert(removedItemBuilder != null),
_items = new List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final dynamic removedItemBuilder;
final List<E> _items;
AnimatedListState get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
// insertItem 方法沒(méi)有 builder 參數(shù),它直接將新插入的元素傳給 AnimatedList 的 builder 方法來(lái)插入新的元素,這樣能夠保持和列表新增元素的動(dòng)效一致。
_animatedList.insertItem(index);
}
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
// 傳入?yún)?shù):移除元素的下標(biāo) 和 一個(gè)構(gòu)建移除元素的方法builder。之所以要這個(gè)方法是因?yàn)樵貙?shí)際從列表馬上移除的,為了在動(dòng)畫(huà)過(guò)渡時(shí)間內(nèi)還能夠看到被移除的元素,需要通過(guò)這種方式來(lái)構(gòu)建一個(gè)被移除的元素來(lái)感覺(jué)是動(dòng)畫(huà)刪除的。這里也可以使用 animation 參數(shù)自定義動(dòng)畫(huà)效果。
_animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
});
}
return removedItem;
}
int get length => _items.length;
E operator [](int index) => _items[index];
int indexOf(E item) => _items.indexOf(item);
}
// 列表項(xiàng)
class CardItem extends StatelessWidget {
const CardItem({
Key key,
@required this.animation,
this.onTap,
@required this.item,
this.selected: false
}) : assert(animation != null),
assert(item != null && item >= 0),
assert(selected != null),
super(key: key);
final Animation<double> animation;
final VoidCallback onTap;
final int item;
final bool selected;
@override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.display1;
if (selected)
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
return new Padding(
padding: const EdgeInsets.all(2.0),
child: new SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: new SizedBox(
height: 128.0,
child: new Card(
color: Colors.primaries[item % Colors.primaries.length],
child: new Center(
child: new Text('Item $item', style: textStyle),
),
),
),
),
),
);
}
}
void main() {
runApp(new AnimatedListSample());
}
2. SingleChildScrollView (只能接收一個(gè)子組件)
內(nèi)容不會(huì)超過(guò)屏幕太多時(shí)使用,不支持基于Sliver的延遲實(shí)例化模型。
SingleChildScrollView({
// 公共參數(shù)
Key? key,
this.scrollDirection = Axis.vertical, //
this.reverse = false, //
this.padding, //
bool? primary, //
this.physics, //
this.controller, //
this.child, //
//
this.dragStartBehavior = DragStartBehavior.start,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
})
示例(將大寫(xiě)字母A-Z沿垂直方向顯示)
class SingleChildScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Scrollbar( // 顯示進(jìn)度條
child: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Center(
child: Column(
// 動(dòng)態(tài)創(chuàng)建一個(gè)List<Widget>
children: str.split("")
// 每一個(gè)字母都用一個(gè)Text顯示,字體為原來(lái)的兩倍
.map((c) => Text(c, textScaleFactor: 2.0,))
.toList(),
),
),
),
);
}
}

3. GridView(二維網(wǎng)格列表)
GridView({
Axis scrollDirection = Axis.vertical, // 滾動(dòng)方向
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding, // 內(nèi)邊距
// 控制子組件如何排列
// SliverGridDelegate定義了GridView Layout相關(guān)接口,子類(lèi)需要通過(guò)實(shí)現(xiàn)它們來(lái)實(shí)現(xiàn)具體的布局算法。
// SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。
@required SliverGridDelegate gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
List<Widget> children = const <Widget>[], // 子列表
})
和ListView的大多數(shù)參數(shù)都是相同的。
- SliverGridDelegateWithFixedCrossAxisCount、GridView.count
橫軸為固定數(shù)量子元素。
// GridView.count構(gòu)造函數(shù)內(nèi)部使用了SliverGridDelegateWithFixedCrossAxisCount。
SliverGridDelegateWithFixedCrossAxisCount({
/*
// 橫軸子元素的數(shù)量。
// 子元素在橫軸的長(zhǎng)度=ViewPort橫軸長(zhǎng)度/crossAxisCount。
// 子元素的大小是通過(guò)crossAxisCount和childAspectRatio兩個(gè)參數(shù)共同決定的。這里的子元素指的是子組件的最大顯示空間,確保子組件的實(shí)際大小不要超出子元素的空間。
*/
@required double crossAxisCount,
// 主軸方向的間距
double mainAxisSpacing = 0.0,
// 橫軸方向的間距。
double crossAxisSpacing = 0.0,
// 子元素在橫軸長(zhǎng)度和主軸長(zhǎng)度的比例。
double childAspectRatio = 1.0,
})
示例
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 橫軸三個(gè)子widget
childAspectRatio: 1.0 // 寬高比為1
),
children:<Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast)
]
);
上面的示例代碼等價(jià)于(GridView.count):
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);

示例(GridView.count)
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Grid List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new GridView.count(
crossAxisCount: 2,
children: new List.generate(100, (index) {
return new Center(
child: new Text(
'Item $index',
style: Theme.of(context).textTheme.headline,
),
);
}),
),
),
);
}
}
- SliverGridDelegateWithMaxCrossAxisExtent、GridView.extent
橫軸子元素為固定最大長(zhǎng)度。
// GridView.extent構(gòu)造函數(shù)內(nèi)部使用了SliverGridDelegateWithMaxCrossAxisExtent。
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 子元素在橫軸上的最大長(zhǎng)度
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
示例
GridView(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0 // 寬高比為2
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
上面的示例代碼等價(jià)于:
GridView.extent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);

- GridView.builder
通過(guò)GridView.builder來(lái)動(dòng)態(tài)創(chuàng)建子widget。
GridView.builder(
...
@required SliverGridDelegate gridDelegate,
@required IndexedWidgetBuilder itemBuilder, // 子widget構(gòu)建器
)
示例
從一個(gè)異步數(shù)據(jù)源(如網(wǎng)絡(luò))分批獲取一些Icon,然后用GridView來(lái)展示
class InfiniteGridView extends StatefulWidget {
@override
_InfiniteGridViewState createState() => new _InfiniteGridViewState();
}
class _InfiniteGridViewState extends State<InfiniteGridView> {
List<IconData> _icons = []; //保存Icon數(shù)據(jù)
@override
void initState() {
// 初始化數(shù)據(jù)
_retrieveIcons();
}
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// mainAxisSpacing: 10.0,
// crossAxisSpacing: 10.0,
crossAxisCount: 3, // 每行三列
childAspectRatio: 1.0 // 顯示區(qū)域?qū)捀呦嗟? ),
itemCount: _icons.length,
itemBuilder: (context, index) {
// 如果顯示到最后一個(gè)并且Icon總數(shù)小于200時(shí)繼續(xù)獲取數(shù)據(jù)
if (index == _icons.length - 1 && _icons.length < 200) {
_retrieveIcons();
}
return Icon(_icons[index]);
}
);
}
void _retrieveIcons() {
Future.delayed(Duration(milliseconds: 200)).then((e) {
setState(() {
_icons.addAll([
Icons.ac_unit,
Icons.airport_shuttle,
Icons.all_inclusive,
Icons.beach_access, Icons.cake,
Icons.free_breakfast
]);
});
});
}
}
