一、ListView組件
在Android中,我們可以使用ListView或RecyclerView來實(shí)現(xiàn),在iOS中,我們可以通過UITableView來實(shí)現(xiàn)。
在Flutter中,我們也有對(duì)應(yīng)的列表Widget,就是ListView。
1.1 ListView基礎(chǔ)
ListView的內(nèi)部繼承順序:
ListViewextendsBoxScrollView—> extendsScrollView—> extendsStatelessWidget
在ListView中,有4種構(gòu)造方法:
-
ListView<Widget>,適合于具有少量子元素的列表視圖 -
ListView.builder,利用IndexedWidgetBuilder來按需構(gòu)造.適合于具有大量子視圖的列表視圖,構(gòu)建器只對(duì)那些實(shí)際可見的子視圖調(diào)用 -
ListView.separated,采用兩個(gè)IndexedWidgetBuilder:itemBuilder根據(jù)需要構(gòu)建子項(xiàng)separatorBuilder類似地構(gòu)建出現(xiàn)在子項(xiàng)之間的分隔符子項(xiàng)。適用于具有固定數(shù)量的子控件的列表視圖 -
ListView.custom,使用SliverChildDelegate構(gòu)造,它提供了定制子模型的其他方面的能力。 例如,SliverChildDelegate可以控制用于估計(jì)實(shí)際上不可見的孩子的大小的算法
1.1.1 ListView<Widget>
ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget
最簡(jiǎn)單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性
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ì)上都是對(duì)自己無能的憤怒。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人活在世界上,不可以有偏差;而且多少要費(fèi)點(diǎn)勁兒,才能把自己保持到理性的軌道上。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("我活在世上,無非想要明白些道理,遇見些有趣的事。", style: textStyle),
)
],
);
}
}
1.1.1.1 ListTile的使用
類似通訊錄的列表,我們可以通過ListTile類實(shí)現(xiàn)
class ListViewDemo extends StatelessWidget {
const ListViewDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
children: List.generate(255, (index) {
return ListTile(
leading: Icon(Icons.favorite),
trailing: Icon(Icons.pets),
title: Text("聯(lián)系人 ${index + 1}"),
subtitle: Text("聯(lián)系人電話號(hào)碼"),
);
})
);
}
}
1.1.1.2 比較重要的屬性 scrollDirection、 itemExtent
scrollDirection:控制列表的滾動(dòng)方向
itemExtent:設(shè)置每一個(gè)item的高度(如果是Axis.horizontal,則為寬度)
reverse:翻轉(zhuǎn)屬性,默認(rèn)為false(從最底部開始排列)
更多的看源碼,嘗試。不做過多介紹
通過上面的兩個(gè)示例,想必你已知曉。默認(rèn)會(huì)創(chuàng)建出所有的childWidget,這樣無疑會(huì)增加性能的開銷. 對(duì)于更多數(shù)量未知的情況,并不適用
1.1.2 ListView.builder
ListView.builder方法有兩個(gè)重要的參數(shù):
- itemBuilder(必傳) 按需構(gòu)造
- itemCount 數(shù)量
class ListViewBuilderDemo extends StatelessWidget {
const ListViewBuilderDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder:(BuildContext context, int index){
return ListTile(title: Text("標(biāo)題$index"), subtitle: Text("詳情內(nèi)容$index"));
},
itemCount: 20,
itemExtent: 30,
);
}
}
1.1.3 ListView.separated
ListView.separated可以生成列表項(xiàng)之間的分割器,它除了比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割器生成器
示例:奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條紅色下劃線
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
);
}
}
示例2:在指定區(qū)域內(nèi),以Icon為分隔器
class ListViewSeparatedDemo extends StatelessWidget {
const ListViewSeparatedDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 300,
child: ListView.separated(
itemBuilder: (BuildContext ctx, int index) {
return Container(
height: 40,
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
},
separatorBuilder: (BuildContext ctx, int index) {
return Icon(Icons.pets,size: 40,);
},
itemCount: 100,
),
);
}
}
二、GridView組件
在iOS中,我們可以通過UICollectionView來實(shí)現(xiàn)多列。在Flutter中也有對(duì)應(yīng)的列表Widget,就是GridView,使用方式和ListView也比較相似。
2.1 GridView基礎(chǔ)
GridView的內(nèi)部繼承順序:
GridViewextendsBoxScrollView—> extendsScrollView—> extendsStatelessWidget可以對(duì)比得知,GridView與ListView繼承于BoxScrollView,所以在很多方面二者是極其相似的
在GridView中,有4種構(gòu)造方法:
-
GridView<Widget>,相對(duì)于ListView多gridDelegate這個(gè)非常特殊的參數(shù) -
GridView.count,GridView.extent(類比上面,可以不用設(shè)置delegate) -
GridView.builder, -
GridView.custom,
2.1.1 GridView<Widget>
gridDelegate:控制交叉軸的item數(shù)量或者寬度,需要傳入的類型是SliverGridDelegate
SliverGridDelegate是一個(gè)抽象類,我們找到它的兩個(gè)子類:
-
SliverGridDelegateWithFixedCrossAxisCount控制交叉軸的item數(shù)量 -
SliverGridDelegateWithMaxCrossAxisExtent控制交叉軸的item的最大寬度
SliverGridDelegateWithFixedCrossAxisCount:包含參數(shù)
@required this.crossAxisCount,//
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
SliverGridDelegateWithMaxCrossAxisExtent:包含參數(shù)
@required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
代碼演示:
//SliverGridDelegateWithMaxCrossAxisExtent示例
class GridViewDelegateDemo extends StatelessWidget {
const GridViewDelegateDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 1.5,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
//SliverGridDelegateWithFixedCrossAxisCount示例
class GridViewDemo extends StatelessWidget {
const GridViewDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5),
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 15 / 9.0,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
2.1.2 GridView.count, GridView.extent
??上面這兩個(gè)構(gòu)造函數(shù),有這對(duì)應(yīng)的簡(jiǎn)寫方式,即GridView.count, GridView.extent構(gòu)造函數(shù)內(nèi)部實(shí)現(xiàn)了對(duì)應(yīng)的delegate
沒有什么好講的,直接上代碼:
class GridViewCountDemo extends StatelessWidget {
const GridViewCountDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.9,
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
class GridViewExtentDemo extends StatelessWidget {
const GridViewExtentDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView.extent(
maxCrossAxisExtent: 200,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.9,
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
));
}
}
2.1.3 GridView.builder
類似ListView.builder,可以使用GridView.build來交給GridView自己管理需要?jiǎng)?chuàng)建的子Widget,降低性能消耗
class GrideViewBuilderDemo extends StatelessWidget {
const GrideViewBuilderDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemBuilder: (BuildContext ctx, int index) {
return Container(
height: 40,
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
});
}
}
2.1.4 GridView.custom
在源碼中,我們可以看到上面的構(gòu)造方法,設(shè)置了SliverChildListDelegate,而GridView.custom則是需要自己去設(shè)置
class GrideViewCustomDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.custom(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
childrenDelegate: SliverChildListDelegate(
List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256))
);
}),
addAutomaticKeepAlives: false,
),
);
}
}
三、Slivers(裂片)
設(shè)想一下平常很常見的視圖布局:一個(gè)滑動(dòng)的視圖中包括一個(gè)標(biāo)題視圖(HeaderView),一個(gè)列表視圖(ListView),一個(gè)網(wǎng)格視圖(GridView),如何讓它們做到統(tǒng)一滑動(dòng)呢?
Flutter中有一個(gè)可以完成這樣滾動(dòng)效果的Widget:CustomScrollView,可以統(tǒng)一管理多個(gè)滾動(dòng)視圖。
在CustomScrollView中,每一個(gè)獨(dú)立的,可滾動(dòng)的Widget被稱之為Sliver。
3.1 Slivers的使用
需要通過CustomScrollView來管理Slivers通過slivers屬性,放數(shù)量不定的Sliver:
Sliver的種類:
-
SliverList:類似于我們之前使用過的ListView; -
SliverGrid:類似于我們之前使用過的GridView; -
SliverFixedExtentList:類似于SliverList,只是可以設(shè)置item的高度; -
SliverAppBar:添加一個(gè)AppBar,包裹Slive,作為CustomScrollView的HeaderView;
給Sliver修改一些顯示區(qū)域布局:
-
SliverPadding:包裹Slive,設(shè)置Sliver的內(nèi)邊距; -
SliverSafeArea:包裹Slive,設(shè)置內(nèi)容顯示安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容)
示例:
class SliverSingleDemo extends StatelessWidget {
const SliverSingleDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.only(top: 10,left: 10,right: 10),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
height: 40,
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
},
childCount: 100,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 5,
childAspectRatio: 1.5),
),
),
)
],
);
}
}
示例:SliverAppBar + SliverGrid + SliverFixedExtentList + SliverPadding+SliverSafeArea
class MutiSliverDemo extends StatelessWidget {
const MutiSliverDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,//懸停效果
expandedHeight: 300,//高度
flexibleSpace: FlexibleSpaceBar(//靈活的headview
title: Text('Sliver demo'),
background: Image.network('https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg',fit: BoxFit.cover,),
),
),
SliverGrid(delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
/*color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),*/
child: new Text('grid item $index'),
);
},
childCount: 10,
), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,),
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20,
))
],
);
}
}
SliverAppBar有很多屬性,有時(shí)間可以自己查看源碼及官方文檔,嘗試一下
四、監(jiān)聽滾動(dòng)事件
在Flutter中監(jiān)聽滾動(dòng)相關(guān)的內(nèi)容由兩部分組成:ScrollController和ScrollNotification。
4.1 ScrollController
- 在Flutter中,Widget并不是最終渲染到屏幕上的元素(渲染的是RenderObject),因此通常這種監(jiān)聽事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過對(duì)應(yīng)的Widget的Controller來實(shí)現(xiàn)。
- 通常情況下,根據(jù)滾動(dòng)的位置來改變一些Widget的狀態(tài)信息,ScrollController通常會(huì)和StatefulWidget一起來使用,并且會(huì)在其中控制它的初始化、監(jiān)聽、銷毀等事件。
- ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動(dòng)事件。
- 手動(dòng)設(shè)置offset通過以下兩個(gè)方法:
-
jumpTo(double offset)、animateTo(double offset,...):這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置,不同之處: 后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫,而前者不會(huì)。
案例1:當(dāng)滾動(dòng)到1000位置的時(shí)候,顯示一個(gè)回到頂部的按鈕
class ZQHomePage extends StatefulWidget {
@override
_ZQHomePageState createState() => _ZQHomePageState();
}
class _ZQHomePageState extends State<ZQHomePage> {
ScrollController _controller = ScrollController(initialScrollOffset: 300);
bool _isShowFloatButton = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
print("監(jiān)聽滾動(dòng)。。。。${_controller.offset}");
setState(() {
_isShowFloatButton = _controller.offset >= 1000;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表測(cè)試'),
),
body: ListView.builder(
controller: _controller,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Icon(Icons.pets),
title: Text("聯(lián)系人$index"),
);
},
itemCount: 300,
),
floatingActionButton: _isShowFloatButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//controller.jumpTo(0);
_controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
},
) : null,
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
);
}
4.2 NotificationListener
通過NotificationListener,可以監(jiān)聽什么時(shí)候開始滾動(dòng),什么時(shí)候結(jié)束滾動(dòng)
-
NotificationListener是一個(gè)Widget,模板參數(shù)T是想監(jiān)聽的通知類型,如果省略,則所有類型通知都會(huì)被監(jiān)聽,如果指定特定類型,則只有該類型的通知會(huì)被監(jiān)聽。 -
NotificationListener需要一個(gè)onNotification回調(diào)函數(shù),用于實(shí)現(xiàn)監(jiān)聽處理邏輯。 - 該回調(diào)可以返回一個(gè)布爾值,代表是否阻止該事件繼續(xù)向上冒泡,如果為
true時(shí),則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false時(shí),則冒泡繼續(xù)。
案例: 列表滾動(dòng), 并且在中間顯示滾動(dòng)進(jìn)度
class ZQNewHomePage extends StatefulWidget {
@override
_ZQNewHomePageState createState() => _ZQNewHomePageState();
}
class _ZQNewHomePageState extends State<ZQNewHomePage> {
ScrollController _controller = ScrollController(initialScrollOffset: 300);
bool _isShowFloatButton = false;
int _progress = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表測(cè)試'),
),
body: NotificationListener(
onNotification: (ScrollNotification notification){
if(notification is ScrollStartNotification){
print("開始滾動(dòng)..");
}else if(notification is ScrollEndNotification){
print("結(jié)束滾動(dòng)..");
}else if(notification is ScrollUpdateNotification){
print("正在滾動(dòng)..");
// 當(dāng)前滾動(dòng)的位置和總長(zhǎng)度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_isShowFloatButton = notification.metrics.pixels >= 1000;
_progress = (progress * 100).toInt();
});
print("當(dāng)前滾動(dòng)位置:${notification.metrics.pixels}");
print("總滾動(dòng)位置:${notification.metrics.maxScrollExtent}");
}
return true;
},
child: Stack(
alignment: Alignment.center,
children:[
ListView.builder(
controller: _controller,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Icon(Icons.pets),
title: Text("聯(lián)系人$index"),
);
},
itemCount: 300,
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
),
floatingActionButton: _isShowFloatButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//controller.jumpTo(0);
_controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
},
) : null,
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_controller.dispose();
}
}