1. 前言
Flutter
作為時下最流行的技術之一,憑藉其出色的效能以及抹平多端的差異優勢,早已引起大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態還沒有完全成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,可以預見將來對Flutter
開發人員的需求也會隨之增長。
無論是為了技術嚐鮮還是以後可能的工作機會,都9102年了,作為一個前端開發者,似乎沒有理由不去嘗試它。正是帶著這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續所有程式碼都會託管在上面,歡迎star,一起學習。這是我寫的Flutter系列文章:
在之前的文章中,我們學習瞭如何使用ListView
和GridView
這兩個滾動型別元件。今天,我們就來學習另一個滾動元件CustomScrollView
及其搭配使用的Sliver
系列元件。掌握了它們,你就可以做一些有趣的滾動效果啦~
2. 必備知識
在進入今天的正題之前,我們先來簡單瞭解下今天的兩個主角CustomScrollView
和Sliver
:CustomScrollView
是Flutter
提供的可以用來自定義滾動效果的元件,它可以像膠水一樣將多個Sliver
粘合在一起。
什麼意思呢?舉個栗子(你也可以點選這裡看youtube
上的一個視訊):
假如頁面中同時存在一個List
和一個Grid
,雖然它們看起來是一個整體,但是由於各自的滾動效果是分離的,所以沒法保證一致的滾動效果。
而使用CustomScrollView
元件作為滾動容器,SliverList
和SliverGrid
分別替代List
和Grid
作為CustomScrollView
的子元件,滾動效果再由CustomScrollView
統一控制,這樣就可以了。
其中SliverList
和SliverGrid
就是我們前面提到的Sliver
系列中的兩員,除此之外,Sliver
家族還有常用的幾個:
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
包裹。
3. 熱身:SliverList / SliverGrid
前面講了那麼多的概念似乎有些枯燥,接下來就讓我們從最簡單的一個例子入手來看看如何使用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
系列的元件。
4. 眼前一亮的SliverAppBar
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
時才會生效。另外,你也可以將這三者進行組合使用。
5. 花樣多變的SliverPersistentHeader
在上一小節中我們見識到了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
元件解決問題。
6. 總結
本文首先介紹了CustomScrollView
和Sliver
系列元件的概念及其關係,接著以SliverList
和SliverGrid
結合的示例說明了其使用方法。然後,又介紹了較常用的SliverAppBar
元件,分別解釋了其float
/snap
/pinned
各自的效果。最後,講解了SliverPersistentHeader
元件的使用方法,並用實際例子加以說明其自定義過渡效果的用法。希望通過本文的介紹,你可以用CustomScrollView
和Sliver
系列元件建立出更有意思的滾動效果~