概述
- ListView
- GridView
- sliver
- 滾動的監(jiān)聽
一、ListView
移動端數(shù)據(jù)量比較大時,我們都是通過列表來進(jìn)行展示的,比如商品數(shù)據(jù)、聊天列表、通信錄、朋友圈等。
在Android
中,我們可以使用ListView
或RecyclerView
來實現(xiàn),在iOS
中,我們可以通過UITableView
來實現(xiàn)。
在Flutter
中,我們也有對應(yīng)的列表Widget,就是ListView
。
-
1.1、ListView 基本創(chuàng)建
ListView可以沿一個方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget。
一種最簡單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。
我們來看一下直接使用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ì)上都是對自己無能的憤怒。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("人活在世界上,不可以有偏差;而且多少要費點勁兒,才能把自己保持到理性的軌道上。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("我活在世上,無非想要明白些道理,遇見些有趣的事。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), ], ); } }
提示:我們可以通過
List.generate
創(chuàng)建子 Widget-
List.generate(100, (index):第一個參數(shù)是加載多少個Widget, 第二個是第幾個
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: List.generate(100, (index) { return Text("Hello World $index"); }) ); } }
-
-
2>、ListTile的使用
在開發(fā)中,我們經(jīng)常見到一種列表,有一個圖標(biāo)或圖片(Icon),有一個標(biāo)題(Title),有一個子標(biāo)題(Subtitle),還有尾部一個圖標(biāo)(Icon)。
這個時候,我們可以使用ListTile來實現(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>、垂直方向滾動,默認(rèn)是垂直方向
我們可以通過設(shè)置scrollDirection
參數(shù)來控制視圖的滾動方向
。
我們通過下面的代碼實現(xiàn)一個水平滾動的內(nèi)容:
這里需要注意,我們需要給Container設(shè)置width,否則它是沒有寬度的,就不能正常顯示。或者我們也可以給ListView設(shè)置一個itemExtent
,該屬性會設(shè)置滾動方向上每個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)建
通過構(gòu)造函數(shù)中的children傳入所有的子Widget有一個問題:默認(rèn)會創(chuàng)建出所有的子Widget。
但是對于用戶來說,一次性構(gòu)建出所有的Widget并不會有什么差異,但是對于我們的程序來說會產(chǎn)生性能問題,而且會增加首屏的渲染時間。
我們可以ListView.build來構(gòu)建子Widget,提供性能。class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( // 創(chuàng)建多少個 row itemCount: 50, // 滾動方向的 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
可以生成列表項之間的分割器
,它除了比ListView.builder多了一個separatorBuilder參數(shù),該參數(shù)是一個分割器生成器。
下面我們看一個例子:奇數(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( // 每個Widget 之間的距離 height: 30, // 距離左邊的距離 indent: 16, // 距離右邊的距離 endIndent: 16, // 每條分割線的高度 thickness: 10, color: index % 2 == 0 ? Colors.red : Colors.green, ); }, ); } }
二、GridView 組件
GridView用于展示多列的展示,在開發(fā)中也非常常見,比如直播App中的主播列表、電商中的商品列表等等。
在Flutter中我們可以使用GridView來實現(xiàn),使用方式和ListView也比較相似。
-
2.1、GridView構(gòu)造函數(shù)
使用GridView的方式就是使用構(gòu)造函數(shù)來創(chuàng)建,和ListView對比有一個特殊的參數(shù):gridDelegate
gridDelegate用于控制交叉軸的item數(shù)量或者寬度,需要傳入的類型是SliverGridDelegate,但是它是一個抽象類,所以我們需要傳入它的子類:-
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({ @requireddouble crossAxisCount, // 交叉軸的item個數(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ù)實現(xiàn)相同的效果 -
-
2.2. GridView.build
和ListView一樣,使用構(gòu)造函數(shù)會一次性創(chuàng)建所有的子Widget,會帶來性能問題,所以我們可以使用GridView.build來交給GridView自己管理需要創(chuàng)建的子Widget。
我們直接使用之前的數(shù)據(jù)來進(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 的簡單介紹
我們考慮一個這樣的布局:一個滑動的視圖中包括一個標(biāo)題視圖(HeaderView),一個列表視圖(ListView),一個網(wǎng)格視圖(GridView)。
我們怎么可以讓它們做到統(tǒng)一的滑動效果呢?使用前面的滾動是很難做到的。
Flutter中有一個可以完成這樣滾動效果的Widget:CustomScrollView,可以統(tǒng)一管理多個滾動視圖。
在CustomScrollView中,每一個獨立的,可滾動的Widget被稱之為Sliver。
補(bǔ)充:Sliver可以翻譯成裂片、薄片,你可以將每一個獨立的滾動視圖當(dāng)做一個小裂片。-
3.2、Slivers 的基本使用
因為我們需要把很多的Sliver放在一個CustomScrollView中,所以CustomScrollView有一個slivers屬性,里面讓我們放對應(yīng)的一些Sliver:不可以放棄他的SliverList:類似于我們之前使用過的ListView;
SliverFixedExtentList:類似于SliverList只是可以設(shè)置滾動的高度;
SliverGrid:類似于我們之前使用過的GridView;
SliverPadding:設(shè)置Sliver的內(nèi)邊距,因為可能要單獨給Sliver設(shè)置內(nèi)邊距;
SliverAppBar:添加一個AppBar,通常用來作為CustomScrollView的HeaderView;
-
SliverSafeArea:設(shè)置內(nèi)容顯示在安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容),也就是可以
滾動過安全區(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è)置
多個slivers的使用:SliverAppBar、SliverGrid、SliverList 的設(shè)置class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverAppBar( // true: bar不動 // false: bar動 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 ), ) ], ); } }
四、滾動的監(jiān)聽
對于滾動的視圖,我們經(jīng)常需要監(jiān)聽它的一些滾動事件,在監(jiān)聽到的時候去做對應(yīng)的一些事情。
比如視圖滾動到底部時,我們可能希望做上拉加載更多;
比如滾動到一定位置時顯示一個回到頂部的按鈕,點擊回到頂部的按鈕,回到頂部;
比如監(jiān)聽滾動什么時候開始,什么時候結(jié)束;
在 Flutter 中監(jiān)聽滾動相關(guān)的內(nèi)容由兩部分組成:ScrollController
和ScrollNotification
。
-
4.1、ScrollController 監(jiān)聽,可以預(yù)先設(shè)置offset,也可以監(jiān)聽滾動的位置,
缺點
是:無法檢測股東開始和結(jié)束
在Flutter中,Widget并不是最終渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常這種監(jiān)聽事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過對應(yīng)的Widget的Controller來實現(xiàn)。
ListView、GridView的組件控制器是ScrollController,我們可以通過它來獲取視圖的滾動信息,并且可以調(diào)用里面的方法來更新視圖的滾動位置。
另外,通常情況下,我們會根據(jù)滾動的位置來改變一些Widget的狀態(tài)信息,所以ScrollController通常會和StatefulWidget一起來使用,并且會在其中控制它的初始化、監(jiān)聽、銷毀等事件。
我們來做一個案例,當(dāng)滾動到500位置的時候,顯示一個回到頂部的按鈕:jumpTo(double offset)、animateTo(double offset,...):
這兩個方法用于跳轉(zhuǎn)到指定的位置,它們不同之處在于,后者在跳轉(zhuǎn)時會執(zhí)行一個動畫,而前者不會。-
ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動事件。
代碼如下
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)聽到滾動"); setState(() { _isFloatingActionButton = _controller.offset > 500 ? true : false; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表滾動測試"), ), body: ListView.builder( controller: _controller, itemCount: 20, itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("測試 $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)聽什么時候開始滾動,什么時候結(jié)束滾動,這個時候我們可以通過NotificationListener。- NotificationListener是一個Widget,模板參數(shù)T是想監(jiān)聽的通知類型,如果省略,則所有類型通知都會被監(jiān)聽,如果指定特定類型,則只有該類型的通知會被監(jiān)聽。
- NotificationListener需要一個onNotification回調(diào)函數(shù),用于實現(xiàn)監(jiān)聽處理邏輯。
該回調(diào)可以返回一個布爾值,代表是
false
阻止該事件繼續(xù)向上冒泡,如果為true
時,則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時,則冒泡繼續(xù)。
案例: 列表滾動, 并且在中間顯示滾動進(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("列表滾動測試"), ), body: NotificationListener( onNotification: (ScrollNotification notification ) { if (notification is ScrollStartNotification) { print("----開始滾動----"); } else if (notification is ScrollUpdateNotification) { // 當(dāng)前滾動的位置和總長度 final currentPixel = notification.metrics.pixels; final totalPixel = notification.metrics.maxScrollExtent; double progress = currentPixel / totalPixel; setState(() { _progress = (progress * 100).toInt(); }); print("正在滾動:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}"); } else if (notification is ScrollEndNotification) { print("----結(jié)束滾動----"); } 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, ) ], ), ), ); } }