列表是移動端經(jīng)常使用的一種視圖展示方式蝶棋,在Flutter中提供了ListView和GridView。
一忽妒、ListView組件
移動端數(shù)據(jù)量比較大時玩裙,我們都是通過列表來進行展示的兼贸,比如商品數(shù)據(jù)、聊天列表吃溅、通信錄溶诞、朋友圈等。在iOS中决侈,我們可以通過UITableView來實現(xiàn)螺垢。
ListView可以沿一個方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget赖歌。
一種最簡單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可枉圃。
1、ListView的使用庐冯。
為了讓文字之間有一些間距孽亲,我使用了Padding Widget。
class MyHomeBody extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人的一切痛苦展父,本質(zhì)上都是對自己無能的憤怒返劲。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人活在世界上,不可以有偏差栖茉;而且多少要費點勁兒篮绿,才能把自己保持到理性的軌道上。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("我活在世上吕漂,無非想要明白些道理搔耕,遇見些有趣的事。", style: textStyle),
)
],
);
}
}
2痰娱、ListTile的使用
使用場景:在開發(fā)中弃榨,我們經(jīng)常見到一種列表,有一個圖標(biāo)或圖片(Icon)梨睁,有一個標(biāo)題(Title)鲸睛,有一個子標(biāo)題(Subtitle),還有尾部一個圖標(biāo)(Icon)坡贺。
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.people, size: 36,),
title: Text("聯(lián)系人"),
subtitle: Text("聯(lián)系人信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.email, size: 36,),
title: Text("郵箱"),
subtitle: Text("郵箱地址信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text("消息"),
subtitle: Text("消息詳情信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.map, size: 36,),
title: Text("地址"),
subtitle: Text("地址詳情信息"),
trailing: Icon(Icons.arrow_forward_ios),
)
],
);
}
}
我們可以通過設(shè)置 scrollDirection 參數(shù)來控制視圖的滾動方向官辈。
這里需要注意,我們需要給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),
],
);
}
}
3、ListView.build
3.1 ListView的問題:
通過構(gòu)造函數(shù)中的children傳入所有的子Widget有一個問題:默認(rèn)會創(chuàng)建出所有的子Widget隔节。
但是對于用戶來說鹅经,一次性構(gòu)建出所有的Widget并不會有什么差異寂呛,但是對于我們的程序來說會產(chǎn)生性能問題,而且會增加首屏的渲染時間瘾晃。
我們可以ListView.build來構(gòu)建子Widget贷痪,提供性能。
3.2 ListView.build使用場景:
ListView.build適用于子Widget比較多的場景蹦误,該構(gòu)造函數(shù)將創(chuàng)建子Widget交給了一個抽象的方法劫拢,交給ListView進行管理,ListView會在真正需要的時候去創(chuàng)建子Widget强胰,而不是一開始就全部初始化好舱沧。
該方法有兩個重要參數(shù):
a、itemBuilder:列表項創(chuàng)建的方法哪廓。當(dāng)列表滾動到對應(yīng)位置的時候狗唉,ListView會自動調(diào)用該方法來創(chuàng)建對應(yīng)的子Widget初烘。類型是IndexedWidgetBuilder涡真,是一個函數(shù)類型。
b肾筐、itemCount:表示列表項的數(shù)量哆料,如果為空,則表示ListView為無限列表吗铐。
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 80,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("標(biāo)題$index"), subtitle: Text("詳情內(nèi)容$index"));
}
);
}
}
3.3 ListView.build動態(tài)數(shù)據(jù)
思考:這個時候是否依然可以使用StatelessWidget:
答案:不可以东亦,因為當(dāng)前我們的數(shù)據(jù)是異步加載的,剛開始界面并不會展示數(shù)據(jù)(沒有數(shù)據(jù))唬渗,后面從JSON中加載出來數(shù)據(jù)(有數(shù)據(jù))后典阵,再次展示加載的數(shù)據(jù)。
這里是有狀態(tài)的變化的镊逝,從無數(shù)據(jù)壮啊,到有數(shù)據(jù)的變化。
這個時候撑蒜,我們需要使用StatefulWidget來管理組件歹啼。
3.4 ListView.separated
ListView.separated可以生成列表項之間的分割器,它除了比ListView.builder多了一個separatorBuilder參數(shù)座菠,該參數(shù)是一個分割器生成器狸眼。
實現(xiàn)奇偶行分割線不同的顏色:
class MySeparatedDemo extends StatelessWidget {
Divider blueColor = Divider(color: Colors.blue);
Divider redColor = Divider(color: Colors.red);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("聯(lián)系人${index+1}"),
subtitle: Text("聯(lián)系人電話${index+1}"),
);
},
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? redColor : blueColor;
},
itemCount: 100
);
}
}
二、GridView組件
GridView用于展示多列的展示浴滴,在開發(fā)中也非常常見拓萌,比如直播App中的主播列表、電商中的商品列表等等升略。iOS中類似的控件為UICollectionView司志。
1甜紫、GridView構(gòu)造函數(shù)
一種使用GridView的方式就是使用構(gòu)造函數(shù)來創(chuàng)建,和ListView對比有一個特殊的參數(shù):gridDelegate 骂远, 它用于控制交叉軸的item數(shù)量或者寬度囚霸,需要傳入的類型是SliverGridDelegate,但是它是一個抽象類激才,所以我們需要傳入它的子類:SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount, // 交叉軸的item個數(shù)
double mainAxisSpacing = 0.0, // 主軸的間距
double crossAxisSpacing = 0.0, // 交叉軸的間距
double childAspectRatio = 1.0, // 子Widget的寬高比
})
代碼:
class MyGridCountDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
SliverGridDelegateWithMaxCrossAxisExtent:
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 交叉軸的item寬度
double mainAxisSpacing = 0.0, // 主軸的間距
double crossAxisSpacing = 0.0, // 交叉軸的間距
double childAspectRatio = 1.0, // 子Widget的寬高比
})
代碼:
class MyGridExtentDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
這兩種方式也可以不設(shè)置delegate拓型,可以分別使用:GridView.count構(gòu)造函數(shù)和GridView.extent構(gòu)造函數(shù)實現(xiàn)相同的效果。
2瘸恼、GridView.build
GridView.build和ListView一樣劣挫,使用構(gòu)造函數(shù)會一次性創(chuàng)建所有的子Widget,會帶來性能問題东帅,所以我們可以使用GridView.build來交給GridView自己管理需要創(chuàng)建的子Widget压固。
class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
List<Anchor> anchors = [];
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.2
),
itemCount: anchors.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.network(anchors[index].imageUrl),
SizedBox(height: 5),
Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
],
),
);
}
),
);
}
}
三、Sliver
場景:一個滑動的視圖中包括一個標(biāo)題視圖(HeaderView)靠闭,一個列表視圖(ListView)帐我,一個網(wǎng)格視圖(GridView)。我們怎么可以讓它們做到統(tǒng)一的滑動效果呢愧膀?
使用前面的滾動是很難做到的拦键。Flutter中有一個可以完成這樣滾動效果的Widget:CustomScrollView,可以統(tǒng)一管理多個滾動視圖檩淋。
在CustomScrollView中芬为,每一個獨立的,可滾動的Widget被稱之為Sliver蟀悦。
補充:Sliver可以翻譯成裂片媚朦、薄片,你可以將每一個獨立的滾動視圖當(dāng)做一個小裂片日戈。
3.1 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)容
SliverGrid+SliverPadding+SliverSafeArea的組合:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment(0, 0),
color: Colors.orange,
child: Text("item$index"),
);
},
childCount: 20
),
),
),
)
],
);
}
}
3.2 Slivers的組合使用
SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Coderwhy Demo'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 10,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20
),
),
],
);
}
}
四钮糖、監(jiān)聽滾動視圖
對于滾動的視圖,我們經(jīng)常需要監(jiān)聽它的一些滾動事件,在監(jiān)聽到的時候去做對應(yīng)的一些事情店归。eg:
a阎抒、視圖滾動到底部時,我們可能希望做上拉加載更多消痛;
b且叁、滾動到一定位置時顯示一個回到頂部的按鈕,點擊回到頂部的按鈕秩伞,回到頂部逞带;
c、監(jiān)聽滾動什么時候開始纱新,什么時候結(jié)束展氓;
在Flutter中監(jiān)聽滾動相關(guān)的內(nèi)容由兩部分組成:ScrollController和ScrollNotification。
4.1 ScrollController
在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)滾動到1000位置的時候,顯示一個回到頂部的按鈕:
jumpTo(double offset)却盘、animateTo(double offset,...):這兩個方法用于跳轉(zhuǎn)到指定的位置狰域,它們不同之處在于,后者在跳轉(zhuǎn)時會執(zhí)行一個動畫黄橘,而前者不會兆览。
ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動事件塞关。
class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController _controller;
bool _isShowTop = false;
@override
void initState() {
// 初始化ScrollController
_controller = ScrollController();
// 監(jiān)聽滾動
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展示"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
4.2 NotificationListener
場景:監(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)可以返回一個布爾值,代表是否阻止該事件繼續(xù)向上冒泡孩饼,如果為true時髓削,則冒泡終止,事件停止向上傳播镀娶,如果不返回或者返回值為false 時立膛,則冒泡繼續(xù)。
案例: 列表滾動, 并且在中間顯示滾動進度:
class MyHomeNotificationDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}
class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判斷監(jiān)聽事件的類型
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 false;
},
child: Stack(
alignment: Alignment(.9, .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,
)
],
),
);
}
}