概述
- ListView
- GridView
- sliver
- 滾動(dòng)的監(jiān)聽(tīng)
一、ListView
移動(dòng)端數(shù)據(jù)量比較大時(shí),我們都是通過(guò)列表來(lái)進(jìn)行展示的,比如商品數(shù)據(jù)、聊天列表、通信錄、朋友圈等。
在Android
中,我們可以使用ListView
或RecyclerView
來(lái)實(shí)現(xiàn),在iOS
中,我們可以通過(guò)UITableView
來(lái)實(shí)現(xiàn)。
在Flutter
中,我們也有對(duì)應(yīng)的列表Widget,就是ListView
。
-
1.1、ListView 基本創(chuàng)建
ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來(lái)排列其所有子Widget。
一種最簡(jiǎn)單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。
我們來(lái)看一下直接使用ListView的代碼演練:-
1>、為了讓文字之間有一些間距,我使用了Padding Widget
ListView的基本創(chuàng)建class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: <Widget>[ Padding( padding: const EdgeInsets.all(20.0), child: Text("人的一切痛苦,本質(zhì)上都是對(duì)自己無(wú)能的憤怒。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("人活在世界上,不可以有偏差;而且多少要費(fèi)點(diǎn)勁兒,才能把自己保持到理性的軌道上。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("我活在世上,無(wú)非想要明白些道理,遇見(jiàn)些有趣的事。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), ], ); } }
提示:我們可以通過(guò)
List.generate
創(chuàng)建子 Widget-
List.generate(100, (index):第一個(gè)參數(shù)是加載多少個(gè)Widget, 第二個(gè)是第幾個(gè)
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: List.generate(100, (index) { return Text("Hello World $index"); }) ); } }
-
-
2>、ListTile的使用
在開(kāi)發(fā)中,我們經(jīng)常見(jiàn)到一種列表,有一個(gè)圖標(biāo)或圖片(Icon),有一個(gè)標(biāo)題(Title),有一個(gè)子標(biāo)題(Subtitle),還有尾部一個(gè)圖標(biāo)(Icon)。
這個(gè)時(shí)候,我們可以使用ListTile來(lái)實(shí)現(xiàn):
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: <Widget>[ ListTile( leading: Icon(Icons.people, size: 20,), title: Text("聯(lián)系人"), subtitle: Text("聯(lián)系人信息"), trailing: Icon(Icons.arrow_right), ), ListTile( leading: Icon(Icons.people, size: 20,), title: Text("郵箱"), subtitle: Text("郵箱地址信息"), trailing: Icon(Icons.arrow_right), ), ], ); } }
-
3>、垂直方向滾動(dòng),默認(rèn)是垂直方向
我們可以通過(guò)設(shè)置scrollDirection
參數(shù)來(lái)控制視圖的滾動(dòng)方向
。
我們通過(guò)下面的代碼實(shí)現(xiàn)一個(gè)水平滾動(dòng)的內(nèi)容:
這里需要注意,我們需要給Container設(shè)置width,否則它是沒(méi)有寬度的,就不能正常顯示。或者我們也可以給ListView設(shè)置一個(gè)itemExtent
,該屬性會(huì)設(shè)置滾動(dòng)方向上每個(gè)item所占據(jù)的寬度
。
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( scrollDirection: Axis.horizontal, itemExtent: 200, children: <Widget>[ Container(color: Colors.red, width: 200), Container(color: Colors.green, width: 200), Container(color: Colors.blue, width: 200), Container(color: Colors.purple, width: 200), Container(color: Colors.orange, width: 200), ], ); } }
-
-
1.2、ListView.build 創(chuàng)建
通過(guò)構(gòu)造函數(shù)中的children傳入所有的子Widget有一個(gè)問(wèn)題:默認(rèn)會(huì)創(chuàng)建出所有的子Widget。
但是對(duì)于用戶來(lái)說(shuō),一次性構(gòu)建出所有的Widget并不會(huì)有什么差異,但是對(duì)于我們的程序來(lái)說(shuō)會(huì)產(chǎn)生性能問(wèn)題,而且會(huì)增加首屏的渲染時(shí)間。
我們可以ListView.build來(lái)構(gòu)建子Widget,提供性能。class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( // 創(chuàng)建多少個(gè) row itemCount: 50, // 滾動(dòng)方向的 row 寬度 itemExtent: 100, // 生成 Widget itemBuilder: (BuildContext ctx, int index) { return ListTile(title: Text("標(biāo)題$index"), subtitle: Text("詳情內(nèi)容$index")); } ); } }
-
1.3、ListView.separated 創(chuàng)建(帶分割線)
ListView.separated
可以生成列表項(xiàng)之間的分割器
,它除了比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割器生成器。
下面我們看一個(gè)例子:奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條紅色下劃線:
ListView.separated 創(chuàng)建(帶分割線)class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), trailing: Icon(Icons.arrow_right), title: Text("聯(lián)系人${index+1}"), subtitle: Text("聯(lián)系人電話${index+1}"), ); }, itemCount: 10, separatorBuilder: (BuildContext context, int index) { return Divider( // 每個(gè)Widget 之間的距離 height: 30, // 距離左邊的距離 indent: 16, // 距離右邊的距離 endIndent: 16, // 每條分割線的高度 thickness: 10, color: index % 2 == 0 ? Colors.red : Colors.green, ); }, ); } }
二、GridView 組件
GridView用于展示多列的展示,在開(kāi)發(fā)中也非常常見(jiàn),比如直播App中的主播列表、電商中的商品列表等等。
在Flutter中我們可以使用GridView來(lái)實(shí)現(xiàn),使用方式和ListView也比較相似。
-
2.1、GridView構(gòu)造函數(shù)
使用GridView的方式就是使用構(gòu)造函數(shù)來(lái)創(chuàng)建,和ListView對(duì)比有一個(gè)特殊的參數(shù):gridDelegate
gridDelegate用于控制交叉軸的item數(shù)量或者寬度,需要傳入的類型是SliverGridDelegate,但是它是一個(gè)抽象類,所以我們需要傳入它的子類:-
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({ @requireddouble crossAxisCount, // 交叉軸的item個(gè)數(shù) double mainAxisSpacing = 0.0, // 主軸的間距 double crossAxisSpacing = 0.0, // 交叉軸的間距 double childAspectRatio = 1.0, // 子Widget的寬高比 })
如下代碼
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 20, mainAxisSpacing: 20, // 寬 / 高 childAspectRatio: 2 ), children: List.generate(100, (index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); } ), ); } }
-
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({ double maxCrossAxisExtent, // 交叉軸的item寬度 double mainAxisSpacing = 0.0, // 主軸的間距 double crossAxisSpacing = 0.0, // 交叉軸的間距 double childAspectRatio = 1.0, // 子Widget的寬高比 })
如下代碼
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent ( maxCrossAxisExtent: 100, mainAxisSpacing: 20, crossAxisSpacing: 20, childAspectRatio: 2 ), children: List.generate(100, (index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); } ), ); } }
提示:
前面兩種方式也可以不設(shè)置delegate,可以分別使用:GridView.count構(gòu)造函數(shù)和GridView.extent構(gòu)造函數(shù)實(shí)現(xiàn)相同的效果 -
-
2.2. GridView.build
和ListView一樣,使用構(gòu)造函數(shù)會(huì)一次性創(chuàng)建所有的子Widget,會(huì)帶來(lái)性能問(wèn)題,所以我們可以使用GridView.build來(lái)交給GridView自己管理需要?jiǎng)?chuàng)建的子Widget。
我們直接使用之前的數(shù)據(jù)來(lái)進(jìn)行代碼演練:
GridView.buildclass MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(5.0), child: GridView.builder( shrinkWrap: true, physics: ClampingScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount ( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.2 ), itemCount: 10, itemBuilder: (BuildContext context, int index) { return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.network('http://image.xcar.com.cn/attachments/a/day_200323/2020032314_59939b0716c40f9be872JrmcP75B4KfO.jpg-app'), SizedBox(height: 5), Text('王三', style: TextStyle(fontSize: 6),), ], ), ); } ), ); } }
三、Sliver
3.1、Sliver 的簡(jiǎn)單介紹
我們考慮一個(gè)這樣的布局:一個(gè)滑動(dòng)的視圖中包括一個(gè)標(biāo)題視圖(HeaderView),一個(gè)列表視圖(ListView),一個(gè)網(wǎng)格視圖(GridView)。
我們?cè)趺纯梢宰屗鼈冏龅浇y(tǒng)一的滑動(dòng)效果呢?使用前面的滾動(dòng)是很難做到的。
Flutter中有一個(gè)可以完成這樣滾動(dòng)效果的Widget:CustomScrollView,可以統(tǒng)一管理多個(gè)滾動(dòng)視圖。
在CustomScrollView中,每一個(gè)獨(dú)立的,可滾動(dòng)的Widget被稱之為Sliver。
補(bǔ)充:Sliver可以翻譯成裂片、薄片,你可以將每一個(gè)獨(dú)立的滾動(dòng)視圖當(dāng)做一個(gè)小裂片。-
3.2、Slivers 的基本使用
因?yàn)槲覀冃枰押芏嗟腟liver放在一個(gè)CustomScrollView中,所以CustomScrollView有一個(gè)slivers屬性,里面讓我們放對(duì)應(yīng)的一些Sliver:不可以放棄他的SliverList:類似于我們之前使用過(guò)的ListView;
SliverFixedExtentList:類似于SliverList只是可以設(shè)置滾動(dòng)的高度;
SliverGrid:類似于我們之前使用過(guò)的GridView;
SliverPadding:設(shè)置Sliver的內(nèi)邊距,因?yàn)榭赡芤獑为?dú)給Sliver設(shè)置內(nèi)邊距;
SliverAppBar:添加一個(gè)AppBar,通常用來(lái)作為CustomScrollView的HeaderView;
-
SliverSafeArea:設(shè)置內(nèi)容顯示在安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容),也就是可以
滾動(dòng)過(guò)安全區(qū)域
class MyHomeBody1 extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, childAspectRatio: 2, mainAxisSpacing: 16 ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); }, childCount: 10 ) ), ], ); } }
-
3.3、Slivers的組合使用:SliverAppBar、SliverGrid、SliverList 的設(shè)置
多個(gè)slivers的使用:SliverAppBar、SliverGrid、SliverList 的設(shè)置class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverAppBar( // true: bar不動(dòng) // false: bar動(dòng) pinned: true, // bar 的高度 expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( title: Text("Hello World!"), background: Image.asset("assets/images/iron.png", fit: BoxFit.cover,), ), ), SliverSafeArea( sliver: SliverPadding( padding: EdgeInsets.all(16), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, childAspectRatio: 2, mainAxisSpacing: 16 ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); }, childCount: 6 ) ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("聯(lián)系人"), ); }, childCount: 20 ), ) ], ); } }
四、滾動(dòng)的監(jiān)聽(tīng)
對(duì)于滾動(dòng)的視圖,我們經(jīng)常需要監(jiān)聽(tīng)它的一些滾動(dòng)事件,在監(jiān)聽(tīng)到的時(shí)候去做對(duì)應(yīng)的一些事情。
比如視圖滾動(dòng)到底部時(shí),我們可能希望做上拉加載更多;
比如滾動(dòng)到一定位置時(shí)顯示一個(gè)回到頂部的按鈕,點(diǎn)擊回到頂部的按鈕,回到頂部;
比如監(jiān)聽(tīng)滾動(dòng)什么時(shí)候開(kāi)始,什么時(shí)候結(jié)束;
在 Flutter 中監(jiān)聽(tīng)滾動(dòng)相關(guān)的內(nèi)容由兩部分組成:ScrollController
和ScrollNotification
。
-
4.1、ScrollController 監(jiān)聽(tīng),可以預(yù)先設(shè)置offset,也可以監(jiān)聽(tīng)滾動(dòng)的位置,
缺點(diǎn)
是:無(wú)法檢測(cè)股東開(kāi)始和結(jié)束
在Flutter中,Widget并不是最終渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常這種監(jiān)聽(tīng)事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過(guò)對(duì)應(yīng)的Widget的Controller來(lái)實(shí)現(xiàn)。
ListView、GridView的組件控制器是ScrollController,我們可以通過(guò)它來(lái)獲取視圖的滾動(dòng)信息,并且可以調(diào)用里面的方法來(lái)更新視圖的滾動(dòng)位置。
另外,通常情況下,我們會(huì)根據(jù)滾動(dòng)的位置來(lái)改變一些Widget的狀態(tài)信息,所以ScrollController通常會(huì)和StatefulWidget一起來(lái)使用,并且會(huì)在其中控制它的初始化、監(jiān)聽(tīng)、銷毀等事件。
我們來(lái)做一個(gè)案例,當(dāng)滾動(dòng)到500位置的時(shí)候,顯示一個(gè)回到頂部的按鈕:jumpTo(double offset)、animateTo(double offset,...):
這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置,它們不同之處在于,后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫(huà),而前者不會(huì)。-
ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來(lái)監(jiān)聽(tīng)滾動(dòng)事件。
代碼如下
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // 設(shè)置變量 _controller 并設(shè)置偏移量 ScrollController _controller = ScrollController(initialScrollOffset: 200); /* 默認(rèn)設(shè)置為 false */ bool _isFloatingActionButton = false; @override void initState() { // TODO: implement initState super.initState(); _controller.addListener(() { print("監(jiān)聽(tīng)到滾動(dòng)"); setState(() { _isFloatingActionButton = _controller.offset > 500 ? true : false; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表滾動(dòng)測(cè)試"), ), body: ListView.builder( controller: _controller, itemCount: 20, itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("測(cè)試 $index"), ); } ), floatingActionButton: _isFloatingActionButton ? FloatingActionButton( child: Icon(Icons.arrow_upward), onPressed: () { // 返回到頂部 _controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn); }, ) : null, ); } }
-
4.2、ScrollNotification
如果我們希望監(jiān)聽(tīng)什么時(shí)候開(kāi)始滾動(dòng),什么時(shí)候結(jié)束滾動(dòng),這個(gè)時(shí)候我們可以通過(guò)NotificationListener。- NotificationListener是一個(gè)Widget,模板參數(shù)T是想監(jiān)聽(tīng)的通知類型,如果省略,則所有類型通知都會(huì)被監(jiān)聽(tīng),如果指定特定類型,則只有該類型的通知會(huì)被監(jiān)聽(tīng)。
- NotificationListener需要一個(gè)onNotification回調(diào)函數(shù),用于實(shí)現(xiàn)監(jiān)聽(tīng)處理邏輯。
該回調(diào)可以返回一個(gè)布爾值,代表是
false
阻止該事件繼續(xù)向上冒泡,如果為true
時(shí),則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時(shí),則冒泡繼續(xù)。
案例: 列表滾動(dòng), 并且在中間顯示滾動(dòng)進(jìn)度class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _progress = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表滾動(dòng)測(cè)試"), ), body: NotificationListener( onNotification: (ScrollNotification notification ) { if (notification is ScrollStartNotification) { print("----開(kāi)始滾動(dòng)----"); } else if (notification is ScrollUpdateNotification) { // 當(dāng)前滾動(dòng)的位置和總長(zhǎng)度 final currentPixel = notification.metrics.pixels; final totalPixel = notification.metrics.maxScrollExtent; double progress = currentPixel / totalPixel; setState(() { _progress = (progress * 100).toInt(); }); print("正在滾動(dòng):${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}"); } else if (notification is ScrollEndNotification) { print("----結(jié)束滾動(dòng)----"); } return true; }, child: Stack( alignment: Alignment(0.9, 0.9), children: <Widget>[ ListView.builder( itemCount: 100, itemExtent: 60, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("item$index")); } ), CircleAvatar( radius: 30, child: Text("$_progress%"), backgroundColor: Colors.black54, ) ], ), ), ); } }