Flutter:Slivers大家族,讓滑動檢視的組合變得很簡單!
文中所有示例程式碼請點選: FlutterUI%2Ftree%2Fmaster%2Flib%2Fsliver" rel="nofollow,noindex">gitee.com/yumi0629/Fl…
今天呢,我小拉麵主要想給大家講一講Flutter中的 Slivers
大家族的使用場景和方法。開發過列表佈局的同學們應該對 Slivers
系列的控制元件不陌生,或多或少都用過這個庫中的控制元件,來解決複雜的滑動巢狀佈局。
比如之前講Hero的時候提到的下面這個介面,使用普通的GridView的話是沒法實現的,我們選擇使用 CustomScrollView
,然後在 slivers
屬性中新增子控制元件,在這個例子裡,我們可以用SliverToBoxAdapter來做HeaderView,GridView來做主體佈局,整體為一個CustomScrollView,完全不會出現任何滑動衝突的問題。

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(), ), ), ], ); 複製程式碼

floating
屬性為
true
,那麼AppBar會在你做出下拉手勢時就立即展開(即使ListView並沒有到達頂部),該展開狀態不顯示flexibleSpace:

floating
和
snap
屬性為
true
,那麼AppBar會在你做出下拉手勢時就立即全部展開(即使ListView並沒有到達頂部),該展開狀態顯示flexibleSpace:

如果不想AppBar消失,則設定 pinned
屬性為 true
即可:

SliverList
SliverList
的使用非常簡單,只需設定 delegate
屬性即可,我們一般使用 SliverChildBuilderDelegate
,注意記得設定 childCount
,否則Flutter沒法知道怎麼繪製:
CustomScrollView( slivers: <Widget>[ SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return _buildItem(context, products[index]); }, childCount: 3, ), ) ], ); 複製程式碼

你也可以通過下面的方式來設定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]);; } ); 複製程式碼

SliverPersistentHeader
SliverPersistentHeader
顧名思義,就是給一個可滑動的檢視新增一個頭(實際上,在CustomScrollView的slivers列表中,header可以出現在檢視的任意位置,不一定要是在頂部)。 這個Header會隨著滑動而展開/收起 ,使用 pinned
和 floating
屬性來控制收起時Header是否展示( pinned
和 floating
屬性不可以同時為 true
), pinned
和 floating
屬性的具體意義和SliverAppBar中相同,這裡就不再次解釋了。

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中。

上圖中框起來的部分全部都是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(), ), ], ); 複製程式碼