Flutter
做爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差別優點,早已引發大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態尚未徹底成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,能夠預見未來對Flutter
開發人員的需求也會隨之增加。前端
不管是爲了技術嚐鮮仍是之後可能的工做機會,都9102年了,做爲一個前端開發者,彷佛沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續全部代碼都會託管在上面,歡迎star,一塊兒學習。這是我寫的Flutter系列文章:react
在以前的文章中,咱們學習瞭如何使用ListView
和GridView
這兩個滾動類型組件。今天,咱們就來學習另外一個滾動組件CustomScrollView
及其搭配使用的Sliver
系列組件。掌握了它們,你就能夠作一些有趣的滾動效果啦~ios
在進入今天的正題以前,咱們先來簡單瞭解下今天的兩個主角CustomScrollView
和Sliver
:CustomScrollView
是Flutter
提供的能夠用來自定義滾動效果的組件,它能夠像膠水同樣將多個Sliver
粘合在一塊兒。git
什麼意思呢?舉個栗子(你也能夠點擊這裏看youtube
上的一個視頻):github
假如頁面中同時存在一個List
和一個Grid
,雖然它們看起來是一個總體,可是因爲各自的滾動效果是分離的,因此無法保證一致的滾動效果。segmentfault
而使用CustomScrollView
組件做爲滾動容器,SliverList
和SliverGrid
分別替代List
和Grid
做爲CustomScrollView
的子組件,滾動效果再由CustomScrollView
統一控制,這樣就能夠了。數組
其中SliverList
和SliverGrid
就是咱們前面提到的Sliver
系列中的兩員,除此以外,Sliver
家族還有經常使用的幾個:app
SliverAppBar
:Creates a material design app bar that can be placed in a CustomScrollView.SliverPersistentHeader
:Creates a sliver that varies its size when it is scrolled to the start of a viewport.SliverFillRemaining
:Creates a sliver that fills the remaining space in the viewport.SliverToBoxAdapter
:Creates a sliver that contains a single box widget.SliverPadding
:Creates a sliver that applies padding on each side of another sliver.注意:因爲CustomeScrollView
的子組件只能是Sliver
系列,因此若是你想將一個普通組件塞進CustomScrollView
,那麼務必將該組件用SliverToBoxAdapter
包裹。ide
前面講了那麼多的概念彷佛有些枯燥,接下來就讓咱們從最簡單的一個例子入手來看看如何使用CustomScrollView
和SliverList
/SliverGrid
。函數
其實CustomScrollView
的用法很簡單,它有一個slivers
屬性,是一個Widget
數組,將子組件都放在裏面就能夠了,其餘的一些滾動相關的屬性基本和咱們以前學到的ListView
差很少。
CustomScrollView( slivers: <Widget>[ renderSliverA(), renderSliverB(), renderSliverC(), ], )
再來看看SliverList
,它只有一個delegate
屬性,能夠用SliverChildListDelegate
或SliverChildBuilderDelegate
這兩個類實現。前者將會一次性所有渲染子組件,後者將會根據視窗渲染當前出現的元素,其效果能夠和ListView
和ListView.build
這兩個構造函數類比。
SliverList( delegate: SliverChildListDelegate( <Widget>[ renderA(), renderB(), renderC(), ] ) ) SliverList( delegate: SliverChildBuilderDelegate( (context, index) => renderItem(context, index), childCount: 10, ) )
經過上面的例子咱們發現SliverList
的使用方式和ListView
大同小異,而SliverGrid
也是如此,這裏就再也不過多贅述,來看個兩列網格的例子:
SliverGrid.count( crossAxisCount: 2, children: <Widget>[ renderA(), renderB(), renderC(), renderD() ] )
接下來,就讓咱們經過一個實際例子將上面的三點結合在一塊兒。
代碼(完整版看這裏):
final List<Color> colorList = [ Colors.red, Colors.orange, Colors.green, Colors.purple, Colors.blue, Colors.yellow, Colors.pink, Colors.teal, Colors.deepPurpleAccent ]; // Text組件須要用SliverToBoxAdapter包裹,才能做爲CustomScrollView的子組件 Widget renderTitle(String title) { return SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Text( title, textAlign: TextAlign.center, style: TextStyle(fontSize: 20), ), ), ); } CustomScrollView( slivers: <Widget>[ renderTitle('SliverGrid'), SliverGrid.count( crossAxisCount: 3, children: colorList.map((color) => Container(color: color)).toList(), ), renderTitle('SliverList'), SliverFixedExtentList( // SliverList的語法糖,用於每一個item固定高度的List delegate: SliverChildBuilderDelegate( (context, index) => Container(color: colorList[index]), childCount: colorList.length, ), itemExtent: 100, ), ], )
效果圖:
上面的例子中還有一點須要注意的是:咱們將標題組件放在了SliverToBoxAdapter
內,由於CustomScrollView
只接受Sliver
系列的組件。
AppBar
是經常使用來構建一個頁面頭部Bar
的組件,在CustomScrollView
中與其對應的是SliverAppBar
組件。它有什麼神奇之處呢?隨着頁面的滾動,頭部Bar
將會有一個收起過渡的效果。咱們先來看下效果:
float效果 | snap效果 | pinned效果 |
---|---|---|
![]() |
![]() |
![]() |
經過上面的預覽圖,想必你確定很好奇SliverAppBar
中的過渡效果是如何實現的~先別急,咱們先來看下應該如何使用它:
SliverAppBar( floating: true, snap: true, pinned: true, expandedHeight: 250, flexibleSpace: FlexibleSpaceBar( title: Text(this.title), background: Image.network( 'http://img1.mukewang.com/5c18cf540001ac8206000338.jpg', fit: BoxFit.cover, ), ), )
SliverAppBar
最重要的幾個屬性在上面的例子中羅列出來。其中:
expandedHeight
:展開狀態下appBar
的高度,即圖中圖片所佔空間;flexibleSpace
:空間大小可變的組件,Flutter
給咱們提供了一個現成的FlexibleSpaceBar
組件,給咱們處理好了title
過渡的效果。另外,floating
/snap
/pinned
這三個屬性能夠指定SliverAppBar
內容滑出屏幕以後的表現形式。
float
:向下滑動時,即便當前CustomScrollView
不在頂部,SliverAppBar
也會跟着一塊兒向下出現;snap
:當手指放開時,SliverAppBar
會根據當前的位置進行調整,始終保持展開
或收起
的狀態;pinned
:不一樣於float
效果,當SliverAppBar
內容滑出屏幕時,將始終渲染一個固定在頂部的收起狀態組件。須要注意的是:snap
效果必定要在float
爲true
時纔會生效。另外,你也能夠將這三者進行組合使用。
在上一小節中咱們見識到了SliverAppBar
的神奇之處,其實它就是基於SliverPersistentHeader
實現的。經過SliverPersistentHeader
,咱們還能夠實現sticky
吸頂的效果。
SliverPersistentHeader
最重要的一個屬性是SliverPersistentHeaderDelegate
,爲此咱們須要實現一個類繼承自SliverPersistentHeaderDelegate
。
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate { @override double get minExtent => null; @override double get maxExtent => null; @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => null; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => null; }
能夠看到,SliverPersistentHeaderDelegate
的實現類必須實現其4個方法。其中:
minExtent
:收起狀態下組件的高度;maxExtent
:展開狀態下組件的高度;shouldRebuild
:相似於react
中的shouldComponentUpdate
;build
:構建渲染的內容。接下來,咱們就來實現一個TabBar
吸頂的效果。
代碼(完整版看這裏):
CustomScrollView( slivers: <Widget>[ SliverAppBar( // ... ), SliverPersistentHeader( // 能夠吸頂的TabBar pinned: true, delegate: StickyTabBarDelegate( child: TabBar( labelColor: Colors.black, controller: this.tabController, tabs: <Widget>[ Tab(text: 'Home'), Tab(text: 'Profile'), ], ), ), ), SliverFillRemaining( // 剩餘補充內容TabBarView child: TabBarView( controller: this.tabController, children: <Widget>[ Center(child: Text('Content of Home')), Center(child: Text('Content of Profile')), ], ), ), ], ) class StickyTabBarDelegate extends SliverPersistentHeaderDelegate { final TabBar child; StickyTabBarDelegate({@required this.child}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return this.child; } @override double get maxExtent => this.child.preferredSize.height; @override double get minExtent => this.child.preferredSize.height; @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { return true; } }
效果圖:
根據上面的圖咱們能夠看到,當下方tab
內容滑出屏幕後,tabBar
並無跟着一塊兒滑走,而是粘在了頂部。可見SliverPersistentHeader
的確能夠知足咱們的sticky
效果。
不過SliverPersistentHeader
的神奇可遠不止如此哦~咱們能夠經過它自定義一些頭部的過渡效果,畢竟SliverAppBar
也是經過它實現的。就好比下方這個電影詳情頁的頭部過渡效果,這在通常的app種仍是比較常見的。
那麼這種效果要如何實現呢?關鍵就在於build
方法中的shrinkOffset
屬性,它表明當前頭部的滾動偏移量。咱們能夠根據它計算獲得當前收起頭部的背景顏色
以及圖標和文案的字體顏色
,這樣就能根據當前位置獲得過渡效果啦~
代碼(完整版看這裏):
class SliverCustomHeaderDelegate extends SliverPersistentHeaderDelegate { final double collapsedHeight; final double expandedHeight; final double paddingTop; final String coverImgUrl; final String title; SliverCustomHeaderDelegate({ this.collapsedHeight, this.expandedHeight, this.paddingTop, this.coverImgUrl, this.title, }); @override double get minExtent => this.collapsedHeight + this.paddingTop; @override double get maxExtent => this.expandedHeight; @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { return true; } Color makeStickyHeaderBgColor(shrinkOffset) { final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt(); return Color.fromARGB(alpha, 255, 255, 255); } Color makeStickyHeaderTextColor(shrinkOffset, isIcon) { if(shrinkOffset <= 50) { return isIcon ? Colors.white : Colors.transparent; } else { final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt(); return Color.fromARGB(alpha, 0, 0, 0); } } @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( height: this.maxExtent, width: MediaQuery.of(context).size.width, child: Stack( fit: StackFit.expand, children: <Widget>[ // 背景圖 Container(child: Image.network(this.coverImgUrl, fit: BoxFit.cover)), // 收起頭部 Positioned( left: 0, right: 0, top: 0, child: Container( color: this.makeStickyHeaderBgColor(shrinkOffset), // 背景顏色 child: SafeArea( bottom: false, child: Container( height: this.collapsedHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ IconButton( icon: Icon( Icons.arrow_back_ios, color: this.makeStickyHeaderTextColor(shrinkOffset, true), // 返回圖標顏色 ), onPressed: () => Navigator.pop(context), ), Text( this.title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, color: this.makeStickyHeaderTextColor(shrinkOffset, false), // 標題顏色 ), ), IconButton( icon: Icon( Icons.share, color: this.makeStickyHeaderTextColor(shrinkOffset, true), // 分享圖標顏色 ), onPressed: () {}, ), ], ), ), ), ), ), ], ), ); } }
上面的代碼雖然很長,但大部分是構建widget
的代碼。因此,咱們重點關注makeStickyHeaderTextColor
和makeStickyHeaderBgColor
便可。這兩個方法都是根據當前的shrinkOffset
值計算過渡過程當中的顏色值。另外,這裏須要注意頭部在iPhoneX
及以上的劉海頭涉及,能夠用SafeArea
組件解決問題。
本文首先介紹了CustomScrollView
和Sliver
系列組件的概念及其關係,接着以SliverList
和SliverGrid
結合的示例說明了其使用方法。而後,又介紹了較經常使用的SliverAppBar
組件,分別解釋了其float
/snap
/pinned
各自的效果。最後,講解了SliverPersistentHeader
組件的使用方法,並用實際例子加以說明其自定義過渡效果的用法。但願經過本文的介紹,你能夠用CustomScrollView
和Sliver
系列組件建立出更有意思的滾動效果~