該文已授權公眾號 「碼個蛋」猖腕,轉載請指明出處
上節(jié)最后留了個坑到這節(jié)來解決拆祈,因為涉及部件比較多,所以留到這邊來繼續(xù)講倘感,不然寫太多了怕小伙伴看不下去
在上節(jié)最后放坏,給小伙伴們展示了 SliveGrid
和 SliverFixedExtentList
的用法,基本上和 GridView
和 ListView
的用法差不多老玛,所以這邊就不多講這兩個部件了淤年。
SliverAppBar
相信很多 Android
開發(fā)的小伙伴會用到 MaterialDesign
的 CollapsingToolbarLayout
來實現(xiàn)折疊頭部,既然 Android
有的蜡豹,那么 Flutter
也不會少麸粮,畢竟 Flutter
主打的也是 MaterialDesign
啊。首先看下 SliverAppBar
的源碼吧镜廉,其實和 AppBar
的參數(shù)差不多弄诲,只是多了一些比較特殊的屬性
const SliverAppBar({
Key key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.flexibleSpace, // 通過這個來設置背景
this.bottom,
this.elevation,
this.forceElevated = false, // 是否顯示層次感
this.backgroundColor,
this.brightness,
this.iconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.expandedHeight, // 展開的高度
// 以下三個等例子再講
this.floating = false,
this.pinned = false,
this.snap = false,
})
別的參數(shù)應該不陌生吧,都是 AppBar
的娇唯,那么直接來看個例子吧齐遵,還是通過上節(jié)說的 CustomScrollView
來包裹 Sliver
部件
class SliverDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: <Widget>[
SliverAppBar(
title: Text('Sliver Demo'),
centerTitle: true,
// 展開的高度
expandedHeight: 300.0,
// 強制顯示陰影
forceElevated: true,
// 設置該屬性,當有下滑手勢的時候塔插,就會顯示 AppBar
// floating: true,
// 該屬性只有在 floating 為 true 的情況下使用梗摇,不然會報錯
// 當上滑到一定的比例,會自動把 AppBar 收縮(不知道是不是 bug佑淀,當 AppBar 下面的部件沒有被 AppBar 覆蓋的時候留美,不會自動收縮)
// 當下滑到一定比例,會自動把 AppBar 展開
// snap: true,
// 設置該屬性使 Appbar 折疊后不消失
// pinned: true,
// 通過這個屬性設置 AppBar 的背景
flexibleSpace: FlexibleSpaceBar(
// title: Text('Expanded Title'),
// 背景折疊動畫
collapseMode: CollapseMode.parallax,
background: Image.asset('images/timg.jpg', fit: BoxFit.cover),
),
),
// 這個部件一般用于最后填充用的伸刃,會占有一個屏幕的高度谎砾,
// 可以在 child 屬性加入需要展示的部件
SliverFillRemaining(
child: Center(child: Text('FillRemaining', style: TextStyle(fontSize: 30.0))),
),
]));
}
}
這里分別給出不同的動圖來查看三個屬性的影響
如果設置了 floating
屬性,當有下拉動作時捧颅,會顯示 AppBar
如果設置了 snap
屬性景图,滑動距離達到一定值后,會根據(jù)滑動方向收縮或者展開
如果設置了 pinned
屬性碉哑,那么 AppBar
就會在界面上不會消失
以上的效果圖把 SliverFillRemaining
換成列表 SliverFixedExtentList
效果可能會更加明顯挚币,這邊給小伙伴自己替換測試吧亮蒋。
SliverFillViewport
這邊提到了 SliverFillRemaining
用來填充視圖,那么順帶提下 SliverFillViewport
這個部件
const SliverFillViewport({
Key key,
@required SliverChildDelegate delegate, // 這個 delegate 同 SliverGrid
this.viewportFraction = 1.0, // 同屏幕的比例值妆毕,1.0 為一個屏幕大小
})
如果一個滑動列表慎玖,每個 item
需要占滿一個屏幕或者更大,可以使用該部件生成列表笛粘,但是如果 item
的高度小于一個屏幕高度趁怔,那就不太推薦了,在首尾會用空白 item
來把未填滿的補上薪前,就是首尾都會留空白润努。我們使用 SliverFillViewport
對 SliverFillRemaning
進行替換
SliverFillViewport(
viewportFraction: 1.0,
delegate: SliverChildBuilderDelegate(
(_, index) => Container(child: Text('Item $index'), alignment: Alignment.center, color: colors[index % 4]),
childCount: 10))
效果就不展示了,可自行運行查看示括。
SliverToBoxAdapter
還記得上節(jié)最后的代碼中铺浇,有使用 SliverToBoxAdapter
這個部件嗎,這個部件只需要傳入一個 child
屬性垛膝。因為在 CustomScrollView
中只允許傳入 Sliver
部件鳍侣,那么類似 Container
等普通部件就不可以使用了,那么這樣就需要更多的 Sliver
組件才能完成視圖繁涂,所以為了方便拱她,直接通過 SliverToBoxAdapter
對普通部件進行包裹,這樣就成為一個 Sliver
部件了扔罪”樱總結下 SliverToBoxAdapter
的功能就是 把一個普通部件包裹成為 Sliver
部件,例子就不舉了矿酵,上節(jié)已經(jīng)有了唬复。
SliverPadding
那么在 CustomScrollView
中部件之間如何設置間距呢,可能你會想到用 SliverToBoxAdapter
包裹一個 Padding
來處理全肮,當然沒問題敞咧。不過 Flutter
也提供了專門的部件 SliverPadding
使用方式同 Padding
,但是需要傳入一個 sliver
作為子類辜腺。
SliverPersistentHeader
Flutter
中休建,為我們提供了這么一個作為頭部的部件 SliverPersistentHeader
,這個部件可以根據(jù)滾動的距離縮小高度评疗,有點類似 SliverAppBar
的背景效果测砂。
const SliverPersistentHeader({
Key key,
@required this.delegate, // SliverPersistentHeaderDelegate,用來創(chuàng)建展示內容
this.pinned = false, // 同 SliverAppBar 屬性
this.floating = false,
})
SliverPersistentHeaderDelegate
這個代理比較特殊百匆,是個抽象類砌些,也就是需要我們自己進行繼承后再實現(xiàn)方法。SliverPersistentHeaderDelegate
需要提供一個最大值,最小值存璃,展示內容仑荐,以及更新部件條件
比如我們需要展示一個最大高度 300,最小高度 100纵东,居中的文字粘招,那么我們可以這么寫這個代理類
class DemoHeader extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.pink,
alignment: Alignment.center,
child: Text('我是一個頭部部件', style: TextStyle(color: Colors.white, fontSize: 30.0)));
} // 頭部展示內容
@override
double get maxExtent => 300.0; // 最大高度
@override
double get minExtent => 100.0; // 最小高度
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; // 因為所有的內容都是固定的,所以不需要更新
}
使用 SliverPersistentHeader
代替 SliverAppBar
篮迎,看下效果
class SliverDemoPage extends StatelessWidget {
final List<Color> colors = [Colors.red, Colors.green, Colors.blue, Colors.pink];
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: <Widget>[
SliverPersistentHeader(delegate: DemoHeader(), pinned: true),
// 這個部件一般用于最后填充用的男图,會占有一個屏幕的高度,
// 可以在 child 屬性加入需要展示的部件
SliverFillRemaining(
child: Center(child: Text('FillRemaining', style: TextStyle(fontSize: 30.0))),
),
]));
}
}
最后的效果圖
當然甜橱,為了方便擴展,需要重新封裝下 Delegate
栈戳,通過外部傳入范圍和展示內容
// 自定義 SliverPersistentHeaderDelegate
class CustomSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final double max; // 最大高度
final double min; // 最小高度
final Widget child; // 需要展示的內容
CustomSliverPersistentHeaderDelegate({@required this.max, @required this.min, @required this.child})
// 如果 assert 內部條件不成立岂傲,會報錯
: assert(max != null),
assert(min != null),
assert(child != null),
assert(min <= max),
super();
// 返回展示的內容,如果內容固定可以直接在這定義子檀,如果需要可擴展镊掖,這邊通過傳入值來定義
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
@override
double get maxExtent => max; // 返回最大高度
@override
double get minExtent => min; // 返回最小高度
@override
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
// 是否需要更新,這里我們定義當高度范圍和展示內容被替換的時候進行刷新界面
return max != oldDelegate.max || min != oldDelegate.min || child != oldDelegate.child;
}
}
然后我們就可以愉快的使用了褂痰,不需要每個 Delegate
都重新寫一遍亩进,例如替換下剛才寫死的 DemoHeader
SliverPersistentHeader(
// 屬性同 SliverAppBar
pinned: true,
floating: true,
// 因為 SliverPersistentHeaderDelegate 是一個抽象類,所以需要自定義
delegate: CustomSliverPersistentHeaderDelegate(
max: 300.0, min: 100.0, child: Text('我是一個頭部部件', style: TextStyle(color: Colors.white, fontSize: 30.0))),
),
例如需要替換成一張圖片缩歪,直接將 Text
修改成 Image
即可归薛。
以上部分代碼查看 sliver_main.dart
文件
NestedScrollView
講到這了,不得不提下 Scrollable
中比較重要的一員 NestedScrollView
匪蝙,先看下官方的解釋
/// A scrolling view inside of which can be nested other scrolling views, with /// their scroll positions being intrinsically linked.
糟透了的翻譯 X 1:一個內部能夠嵌套其他滾動部件主籍,并使其滾動位置聯(lián)結到一起的滾動部件
/// The most common use case for this widget is a scrollable view with a /// flexible [SliverAppBar] containing a [TabBar] in the header (build by /// [headerSliverBuilder], and with a [TabBarView] in the [body], such that the /// scrollable view's contents vary based on which tab is visible.
糟透了的翻譯 X 2:最常用的情況,就是在其 headerSliverBuilder
中使用攜帶 TabBar
的 SliverAppBar
(就是使用 SliverAppBar
的 bottom
屬性添加 tab
切換也)逛球,其 body
屬性使用 TabBarView
來展示 Tab
頁的內容千元,這樣通過切換 Tab
頁就能展示該頁下的展示內容。
看下 headerSliverBuilder
的定義
/// Signature used by [NestedScrollView] for building its header. /// /// The `innerBoxIsScrolled` argument is typically used to control the /// [SliverAppBar.forceElevated] property to ensure that the app bar shows a /// shadow, since it would otherwise not necessarily be aware that it had /// content ostensibly below it. typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContext context, bool innerBoxIsScrolled);
糟透了的翻譯 X 3:用于構建 NestScrollView
的頭部部件颤绕,innerBoxIsScrolled
主要用來控制 SliverAppBar
的 forceElevated
屬性幸海,當內部內容滾動時,顯示 SliverAppbar
的陰影奥务,主要用來提醒內部的內容低于 SliverAppBar
(相當于給人一種物理層次感物独,否則很容易被認為,頭部和內容是連接在一起的)
接下來看下 NestedScrollView
內部個人覺得有點重要的一個方法 sliverOverlapAbsorberHandleFor
/// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor /// [NestedScrollView]. /// /// This is necessary to configure the [SliverOverlapAbsorber] and /// [SliverOverlapInjector] widgets. /// /// For sample code showing how to use this method, see the [NestedScrollView] /// documentation. static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) { final _InheritedNestedScrollView target = context.inheritFromWidgetOfExactType(_InheritedNestedScrollView); assert(target != null, 'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.'); return target.state._absorberHandle; }
請注意到中間的注釋
糟透了的翻譯 X 4:這個方法返回的值對于 SliverOverlapAbsorber
和 SliverOverlapInjector
部件是非常重要的參數(shù)
接著請注意代碼中的那段 assert
中的文字
糟透了的翻譯 X 5:sliverOverlapAbsorberHandleFor
傳入的參數(shù) context
中必須包含 NestedScrollView
SliverOverlapAbsorber
這邊又引入了兩個部件 SliverOverlapAbsorber
+ SliverOverlapInjector
還是看源碼的解釋吧
/// Creates a sliver that absorbs overlap and reports it to a /// [SliverOverlapAbsorberHandle]. /// /// The [handle] must not be null. /// /// The [child] must be a sliver. const SliverOverlapAbsorber({ Key key, @required this.handle, Widget child, })
糟透了的翻譯 X 6:一個 sliver
部件汗洒,用于把部件重疊的高度反饋給 SliverOverlapAbsorberHandle
议纯,而且指明了 handle
不能空,可以通過 NestedScrollView
的 sliverOverlapAbsorberHandleFor
方法來賦值溢谤,并且 child
必須是個 sliver
部件瞻凤,也就是說我們的 SliverAppBar
需要放到 SliverOverlapAbsorber
里面憨攒。
SliverOverlapInjector
/// Creates a sliver that is as tall as the value of the given [handle]'s /// layout extent. /// /// The [handle] must not be null. const SliverOverlapInjector({ Key key, @required this.handle, Widget child, })
糟透了的翻譯 X 7:創(chuàng)建一個和指定的 handle
一樣高度的 sliver
部件,這個 handle
同 SliverOverlapAbsorber
的 handle
保持一致即可阀参。
分析完源碼后肝集,例子的目標很明確,使用 SliverAppBar
+ TabBar
+ TabBarView
蛛壳,先看下最后的效果圖吧
class NestedScrollDemoPage extends StatelessWidget {
final _tabs = <String>['TabA', 'TabB'];
final colors = <Color>[Colors.red, Colors.green, Colors.blue, Colors.pink, Colors.yellow, Colors.deepPurple];
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (context, innerScrolled) => <Widget>[
SliverOverlapAbsorber(
// 傳入 handle 值杏瞻,直接通過 `sliverOverlapAbsorberHandleFor` 獲取即可
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverAppBar(
pinned: true,
title: Text('NestedScroll Demo'),
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(background: Image.asset('images/timg.jpg', fit: BoxFit.cover)),
bottom: TabBar(tabs: _tabs.map((tab) => Text(tab, style: TextStyle(fontSize: 18.0))).toList()),
forceElevated: innerScrolled,
),
)
],
body: TabBarView(
children: _tabs
// 這邊需要通過 Builder 來創(chuàng)建 TabBarView 的內容,否則會報錯
// NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.
.map((tab) => Builder(
builder: (context) => CustomScrollView(
// key 保證唯一性
key: PageStorageKey<String>(tab),
slivers: <Widget>[
// 將子部件同 `SliverAppBar` 重疊部分頂出來衙荐,否則會被遮擋
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverGrid(
delegate: SliverChildBuilderDelegate(
(_, index) => Image.asset('images/ali.jpg'),
childCount: 8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0)),
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(_, index) => Container(
child: Text('$tab - item${index + 1}',
style: TextStyle(fontSize: 20.0, color: colors[index % 6])),
alignment: Alignment.center),
childCount: 15),
itemExtent: 50.0)
],
),
))
.toList()))),
);
}
}
使用的部件和之前講的沒啥大區(qū)別捞挥,就是多了 SliverOverlapAbsorber
和 SliverOverlapInjector
沒啥難度
以上部分代碼查看 nested_scroll_main.dart
文件
sliver
部件常用的也就那么多了,望小伙伴好好吸收忧吟,跟著例子擼擼代碼砌函,擼順下思路
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個項目,實現(xiàn)BLoC
模式溜族,實現(xiàn)狀態(tài)管理:flutter_weather一個課程(當時買了想看下代碼規(guī)范的讹俊,代碼更新會比較慢,雖然是跟著課上的一些寫代碼煌抒,但是還是做了自己的修改仍劈,很多地方看著不舒服,然后就改成自己的實現(xiàn)方式了):flutter_shop
如果對你有幫助的話寡壮,記得給個 Star贩疙,先謝過,你的認可就是支持我繼續(xù)寫下去的動力~