文中所有示例代碼請點擊:https://gitee.com/yumi0629/FlutterUI/tree/master/lib/sliver
今天呢,我小拉麪主要想給大家講一講Flutter中的Slivers
大家族的使用場景和方法。開發過列表佈局的同學們應該對Slivers
系列的控件不陌生,或多或少都用過這個庫中的控件,來解決複雜的滑動嵌套佈局。
比如之前講Hero的時候提到的下面這個界面,使用普通的GridView的話是沒法實現的,我們選擇使用CustomScrollView
,然後在slivers
屬性中添加子控件,在這個例子裏,我們可以用SliverToBoxAdapter來做HeaderView,GridView來做主體佈局,整體爲一個CustomScrollView,完全不會出現任何滑動衝突的問題。
Flutter中的Slivers
大家族基本都是配合CustomScrollView
來實現的,除了上面提到的滑動佈局嵌套,你還可以使用Slivers
來實現頁面頭部展開/收起、
AppBar隨手勢變換等等功能。官方的Sliver庫裏面的控件很多,可以去Flutter API網站搜一下,這篇文章我只講一些常用的控件。
OK, Let's start !!
SliverAppBar
如果你是一名Android開發者,一定使用過CollapsingToolbarLayout
這個佈局來實現AppBar展開/收起的功能,在Flutter裏面則對應SliverAppBar
控件。給SliverAppBar
設置flexibleSpace
和expandedHeight
屬性,就可以輕鬆完成AppBar展開/收起的功能:
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
_buildAction(),
],
title: Text('SliverAppBar'),
backgroundColor: Theme.of(context).accentColor,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.asset('images/food01.jpeg', fit: BoxFit.cover),
),
// floating: floating,
// snap: snap,
// pinned: pinned,
),
SliverFixedExtentList(
itemExtent: 120.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItem(product);
}).toList(),
),
),
],
);
sliver_app_bar_01.gif
如果設置floating
屬性爲true
,那麼AppBar會在你做出下拉手勢時就立即展開(即使ListView並沒有到達頂部),該展開狀態不顯示flexibleSpace:
sliver_app_bar_02.gif
如果同時設置floating
和snap
屬性爲true
,那麼AppBar會在你做出下拉手勢時就立即全部展開(即使ListView並沒有到達頂部),該展開狀態顯示flexibleSpace:
sliver_app_bar_03.gif
如果不想AppBar消失,則設置pinned
屬性爲true
即可:
sliver_app_bar_04.gif
SliverList
SliverList
的使用非常簡單,只需設置delegate
屬性即可,我們一般使用SliverChildBuilderDelegate
,注意記得設置childCount
,否則Flutter沒法知道怎麼繪製:
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItem(context, products[index]);
},
childCount: 3,
),
)
],
);
sliver_list.png
你也可以通過下面的方式來設置childCount,如果不設置childCount,Flutter一旦發現delegate的某個index返回了null,就會認爲childCount就是這個index。
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if(index>products.length){
return null;
}
return _buildItem(context, products[index]);
},
你也可以使用SliverChildListDelegate
來構建delegate:
delegate: SliverChildListDelegate([
_buildItem(),
_buildItem(),
_buildItem(),
]),
SliverChildListDelegate
和SliverChildBuilderDelegate
的區別:
- SliverChildListDelegate一般用來構item建數量明確的列表,會提前build好所有的子item,所以在效率上會有問題,適合item數量不多的情況(不超過一屏)。
- SliverChildBuilderDelegate構建的列表理論上是可以無限長的,因爲使用來lazily construct優化。
(兩者的區別有些類似於ListView和ListView.builder()的區別。)
SliverGrid
SliverGrid
有三個構造函數:SliverGrid.count()
、SliverGrid.extent
和SliverGrid()
。
SliverGrid.count()
指定了一行展示多少個item,下面的例子表示一行展示4個:
SliverGrid.count(children: scrollItems, crossAxisCount: 4)
SliverGrid.extent
可以指定item的最大寬度,然後讓Flutter自己決定一行展示多少個item:
SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 90.0)
SliverGrid()
則是需要指定一個gridDelegate,它提供給了程序員一個自定義Delegate的入口,你可以自己決定每一個item怎麼排列:
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: products.length,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItem(products[index]);;
}
);
sliver_grid.png
SliverPersistentHeader
SliverPersistentHeader
顧名思義,就是給一個可滑動的視圖添加一個頭(實際上,在CustomScrollView的slivers列表中,header可以出現在視圖的任意位置,不一定要是在頂部)。這個Header會隨着滑動而展開/收起,使用pinned
和floating
屬性來控制收起時Header是否展示(pinned
和floating
屬性不可以同時爲true
),pinned
和floating
屬性的具體意義和SliverAppBar中相同,這裏就不再次解釋了。
sliver_persistent_header.gif
SliverPersistentHeader(
pinned: pinned,
floating: floating,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 180.0,
child: Container(),
),
);
構建一個SliverPersistentHeader
需要傳入一個delegate,這個delegate是SliverPersistentHeaderDelegate類型的,而SliverPersistentHeaderDelegate是一個abstract類,我們不能直接new一個SliverPersistentHeaderDelegate出來,因此,我們需要自定義一個delegate來實現SliverPersistentHeaderDelegate類:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
double get minExtent => minHeight;
@override
double get maxExtent => math.max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
寫一個自定義SliverPersistentHeaderDelegate很簡單,只需重寫build()
、get maxExtent
、get minExtent
和shouldRebuild()
這四個方法,上面就是一個最簡單的SliverPersistentHeaderDelegate的實現。其中,maxExtent
表示header完全展開時的高度,minExtent
表示header在收起時的最小高度。因此,對於我們上面的那個自定義Delegate,如果將minHeight
和maxHeight
的值設置爲相同時,header就不會收縮了,這樣的Header跟我們平常理解的Header更像。
之前也提到了,實際使用時,header不一定要放在slivers列表的最前面,可以隨意混搭,當然,一般來說不會有這種視覺需求的:
CustomScrollView(
slivers: <Widget>[
_buildHeader(0),
SliverGrid.count(
crossAxisCount: 3,
children: _products.map((product) {
return _buildItemGrid(product);
}).toList(),
),
_buildHeader(1),
SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItemList(product);
}).toList(),
),
),
_buildHeader(2),
SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 3.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _buildItemGrid2(_products2[index]);
},
childCount: _products2.length,
),
),
],
);
SliverToBoxAdapter
SliverPersistentHeader一般來說都是會展開/收起的(除非minExtent和maxExtent值相同),那麼如果想要在滾動視圖中添加一個普通的控件,那麼就可以使用SliverToBoxAdapter
來將各種視圖組合在一起,放在CustomListView中。
sliver_adapter.png
上圖中框起來的部分全部都是SliverToBoxAdapter,結合SliverToBoxAdapter,滾動視圖可以任意組合:
CustomScrollView(
physics: ScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: _buildHeader(),
),
SliverGrid.count(
crossAxisCount: 3,
children: products.map((product) {
return _buildItemGrid(product);
}).toList(),
),
SliverToBoxAdapter(
child: _buildSearch(),
),
SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildListDelegate(
products.map((product) {
return _buildItemList(product);
}).toList(),
),
),
SliverToBoxAdapter(
child: _buildFooter(),
),
],
);