<meta charset="utf-8">

1. 前言
Flutter作為時(shí)下最流行的技術(shù)之一,憑借其出色的性能以及抹平多端的差異優(yōu)勢(shì),早已引起大批技術(shù)愛(ài)好者的關(guān)注,甚至一些閑魚,美團(tuán),騰訊等大公司均已投入生產(chǎn)使用。雖然目前其生態(tài)還沒(méi)有完全成熟,但身靠背后的Google加持,其發(fā)展速度已經(jīng)足夠驚人,可以預(yù)見(jiàn)將來(lái)對(duì)Flutter開(kāi)發(fā)人員的需求也會(huì)隨之增長(zhǎng)。
無(wú)論是為了技術(shù)嘗鮮還是以后可能的工作機(jī)會(huì),都9102年了,作為一個(gè)前端開(kāi)發(fā)者,似乎沒(méi)有理由不去嘗試它。正是帶著這樣的心理,筆者也開(kāi)始學(xué)習(xí)Flutter,同時(shí)建了一個(gè)用于練習(xí)的倉(cāng)庫(kù),后續(xù)所有代碼都會(huì)托管在上面,歡迎star,一起學(xué)習(xí)。這是我寫的Flutter系列文章:
- 用Flutter構(gòu)建漂亮的UI界面 - 基礎(chǔ)組件篇
- Flutter滾動(dòng)型容器組件 - ListView篇
- Flutter網(wǎng)格型布局 - GridView篇
- 在Flutter中使用自定義Icon
在之前的文章中,我們學(xué)習(xí)了如何使用ListView和GridView這兩個(gè)滾動(dòng)類型組件。今天,我們就來(lái)學(xué)習(xí)另一個(gè)滾動(dòng)組件CustomScrollView及其搭配使用的Sliver系列組件。掌握了它們,你就可以做一些有趣的滾動(dòng)效果啦~
2. 必備知識(shí)
在進(jìn)入今天的正題之前,我們先來(lái)簡(jiǎn)單了解下今天的兩個(gè)主角CustomScrollView和Sliver:CustomScrollView是Flutter提供的可以用來(lái)自定義滾動(dòng)效果的組件,它可以像膠水一樣將多個(gè)Sliver粘合在一起。
什么意思呢?舉個(gè)栗子(你也可以點(diǎn)擊這里看youtube上的一個(gè)視頻):
假如頁(yè)面中同時(shí)存在一個(gè)List和一個(gè)Grid,雖然它們看起來(lái)是一個(gè)整體,但是由于各自的滾動(dòng)效果是分離的,所以沒(méi)法保證一致的滾動(dòng)效果。
而使用CustomScrollView組件作為滾動(dòng)容器,SliverList和SliverGrid分別替代List和Grid作為CustomScrollView的子組件,滾動(dòng)效果再由CustomScrollView統(tǒng)一控制,這樣就可以了。
其中SliverList和SliverGrid就是我們前面提到的Sliver系列中的兩員,除此之外,Sliver家族還有常用的幾個(gè):
-
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系列,所以如果你想將一個(gè)普通組件塞進(jìn)CustomScrollView,那么務(wù)必將該組件用SliverToBoxAdapter包裹。
3. 熱身:SliverList / SliverGrid
前面講了那么多的概念似乎有些枯燥,接下來(lái)就讓我們從最簡(jiǎn)單的一個(gè)例子入手來(lái)看看如何使用CustomScrollView和SliverList/SliverGrid。
其實(shí)CustomScrollView的用法很簡(jiǎn)單,它有一個(gè)slivers屬性,是一個(gè)Widget數(shù)組,將子組件都放在里面就可以了,其他的一些滾動(dòng)相關(guān)的屬性基本和我們之前學(xué)到的ListView差不多。
CustomScrollView(
slivers: <Widget>[
renderSliverA(),
renderSliverB(),
renderSliverC(),
],
)
再來(lái)看看SliverList,它只有一個(gè)delegate屬性,可以用SliverChildListDelegate或SliverChildBuilderDelegate這兩個(gè)類實(shí)現(xiàn)。前者將會(huì)一次性全部渲染子組件,后者將會(huì)根據(jù)視窗渲染當(dāng)前出現(xiàn)的元素,其效果可以和ListView和ListView.build這兩個(gè)構(gòu)造函數(shù)類比。
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
renderA(),
renderB(),
renderC(),
]
)
)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => renderItem(context, index),
childCount: 10,
)
)
通過(guò)上面的例子我們發(fā)現(xiàn)SliverList的使用方式和ListView大同小異,而SliverGrid也是如此,這里就不再過(guò)多贅述,來(lái)看個(gè)兩列網(wǎng)格的例子:
SliverGrid.count(
crossAxisCount: 2,
children: <Widget>[
renderA(),
renderB(),
renderC(),
renderD()
]
)
接下來(lái),就讓我們通過(guò)一個(gè)實(shí)際例子將上面的三點(diǎn)結(jié)合在一起。
代碼(完整版看這里):
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的語(yǔ)法糖,用于每個(gè)item固定高度的List
delegate: SliverChildBuilderDelegate(
(context, index) => Container(color: colorList[index]),
childCount: colorList.length,
),
itemExtent: 100,
),
],
)
效果圖:

上面的例子中還有一點(diǎn)需要注意的是:我們將標(biāo)題組件放在了SliverToBoxAdapter內(nèi),因?yàn)?code>CustomScrollView只接受Sliver系列的組件。
4. 眼前一亮的SliverAppBar
AppBar是常用來(lái)構(gòu)建一個(gè)頁(yè)面頭部Bar的組件,在CustomScrollView中與其對(duì)應(yīng)的是SliverAppBar組件。它有什么神奇之處呢?隨著頁(yè)面的滾動(dòng),頭部Bar將會(huì)有一個(gè)收起過(guò)渡的效果。我們先來(lái)看下效果:
| float效果 | snap效果 | pinned效果 |
|---|---|---|

|

|

|
通過(guò)上面的預(yù)覽圖,想必你肯定很好奇SliverAppBar中的過(guò)渡效果是如何實(shí)現(xiàn)的~先別急,我們先來(lái)看下應(yīng)該如何使用它:
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最重要的幾個(gè)屬性在上面的例子中羅列出來(lái)。其中:
-
expandedHeight:展開(kāi)狀態(tài)下appBar的高度,即圖中圖片所占空間; -
flexibleSpace:空間大小可變的組件,Flutter給我們提供了一個(gè)現(xiàn)成的FlexibleSpaceBar組件,給我們處理好了title過(guò)渡的效果。
另外,floating/snap/pinned這三個(gè)屬性可以指定SliverAppBar內(nèi)容滑出屏幕之后的表現(xiàn)形式。
-
float:向下滑動(dòng)時(shí),即使當(dāng)前CustomScrollView不在頂部,SliverAppBar也會(huì)跟著一起向下出現(xiàn); -
snap:當(dāng)手指放開(kāi)時(shí),SliverAppBar會(huì)根據(jù)當(dāng)前的位置進(jìn)行調(diào)整,始終保持展開(kāi)或收起的狀態(tài); -
pinned:不同于float效果,當(dāng)SliverAppBar內(nèi)容滑出屏幕時(shí),將始終渲染一個(gè)固定在頂部的收起狀態(tài)組件。
需要注意的是:snap效果一定要在float為true時(shí)才會(huì)生效。另外,你也可以將這三者進(jìn)行組合使用。
5. 花樣多變的SliverPersistentHeader
在上一小節(jié)中我們見(jiàn)識(shí)到了SliverAppBar的神奇之處,其實(shí)它就是基于SliverPersistentHeader實(shí)現(xiàn)的。通過(guò)SliverPersistentHeader,我們還可以實(shí)現(xiàn)sticky吸頂?shù)男Ч?/p>
SliverPersistentHeader最重要的一個(gè)屬性是SliverPersistentHeaderDelegate,為此我們需要實(shí)現(xiàn)一個(gè)類繼承自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的實(shí)現(xiàn)類必須實(shí)現(xiàn)其4個(gè)方法。其中:
-
minExtent:收起狀態(tài)下組件的高度; -
maxExtent:展開(kāi)狀態(tài)下組件的高度; -
shouldRebuild:類似于react中的shouldComponentUpdate; -
build:構(gòu)建渲染的內(nèi)容。
接下來(lái),我們就來(lái)實(shí)現(xiàn)一個(gè)TabBar吸頂?shù)男Ч?/p>
代碼(完整版看這里):
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
// ...
),
SliverPersistentHeader( // 可以吸頂?shù)腡abBar
pinned: true,
delegate: StickyTabBarDelegate(
child: TabBar(
labelColor: Colors.black,
controller: this.tabController,
tabs: <Widget>[
Tab(text: 'Home'),
Tab(text: 'Profile'),
],
),
),
),
SliverFillRemaining( // 剩余補(bǔ)充內(nèi)容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;
}
}
效果圖:

根據(jù)上面的圖我們可以看到,當(dāng)下方tab內(nèi)容滑出屏幕后,tabBar并沒(méi)有跟著一起滑走,而是粘在了頂部??梢?jiàn)SliverPersistentHeader的確可以滿足我們的sticky效果。
不過(guò)SliverPersistentHeader的神奇可遠(yuǎn)不止如此哦~我們可以通過(guò)它自定義一些頭部的過(guò)渡效果,畢竟SliverAppBar也是通過(guò)它實(shí)現(xiàn)的。就比如下方這個(gè)電影詳情頁(yè)的頭部過(guò)渡效果,這在一般的app種還是比較常見(jiàn)的。

那么這種效果要如何實(shí)現(xiàn)呢?關(guān)鍵就在于build方法中的shrinkOffset屬性,它代表當(dāng)前頭部的滾動(dòng)偏移量。我們可以根據(jù)它計(jì)算得到當(dāng)前收起頭部的背景顏色以及圖標(biāo)和文案的字體顏色,這樣就能根據(jù)當(dāng)前位置得到過(guò)渡效果啦~
代碼(完整版看這里):
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), // 返回圖標(biāo)顏色
),
onPressed: () => Navigator.pop(context),
),
Text(
this.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: this.makeStickyHeaderTextColor(shrinkOffset, false), // 標(biāo)題顏色
),
),
IconButton(
icon: Icon(
Icons.share,
color: this.makeStickyHeaderTextColor(shrinkOffset, true), // 分享圖標(biāo)顏色
),
onPressed: () {},
),
],
),
),
),
),
),
],
),
);
}
}
上面的代碼雖然很長(zhǎng),但大部分是構(gòu)建widget的代碼。所以,我們重點(diǎn)關(guān)注makeStickyHeaderTextColor和makeStickyHeaderBgColor即可。這兩個(gè)方法都是根據(jù)當(dāng)前的shrinkOffset值計(jì)算過(guò)渡過(guò)程中的顏色值。另外,這里需要注意頭部在iPhoneX及以上的劉海頭涉及,可以用SafeArea組件解決問(wèn)題。
6. 總結(jié)
本文首先介紹了CustomScrollView和Sliver系列組件的概念及其關(guān)系,接著以SliverList和SliverGrid結(jié)合的示例說(shuō)明了其使用方法。然后,又介紹了較常用的SliverAppBar組件,分別解釋了其float/snap/pinned各自的效果。最后,講解了SliverPersistentHeader組件的使用方法,并用實(shí)際例子加以說(shuō)明其自定義過(guò)渡效果的用法。希望通過(guò)本文的介紹,你可以用CustomScrollView和Sliver系列組件創(chuàng)建出更有意思的滾動(dòng)效果~
轉(zhuǎn)自:http://m.itdecent.cn/p/5aeeb7ea776b