Flutter 入門指北(Part 8)之 Sliver 組件、NestedScrollView

該文已授權公眾號 「碼個蛋」猖腕,轉載請指明出處

上節(jié)最后留了個坑到這節(jié)來解決拆祈,因為涉及部件比較多,所以留到這邊來繼續(xù)講倘感,不然寫太多了怕小伙伴看不下去

在上節(jié)最后放坏,給小伙伴們展示了 SliveGridSliverFixedExtentList 的用法,基本上和 GridViewListView 的用法差不多老玛,所以這邊就不多講這兩個部件了淤年。

SliverAppBar

相信很多 Android 開發(fā)的小伙伴會用到 MaterialDesignCollapsingToolbarLayout 來實現(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

floating.gif

如果設置了 snap 屬性景图,滑動距離達到一定值后,會根據(jù)滑動方向收縮或者展開

snap.gif

如果設置了 pinned 屬性碉哑,那么 AppBar 就會在界面上不會消失

pinned.gif

以上的效果圖把 SliverFillRemaining 換成列表 SliverFixedExtentList 效果可能會更加明顯挚币,這邊給小伙伴自己替換測試吧亮蒋。

SliverFillViewport

這邊提到了 SliverFillRemaining 用來填充視圖,那么順帶提下 SliverFillViewport 這個部件

const SliverFillViewport({
    Key key,
    @required SliverChildDelegate delegate, // 這個 delegate 同 SliverGrid 
    this.viewportFraction = 1.0, // 同屏幕的比例值妆毕,1.0 為一個屏幕大小
  })

如果一個滑動列表慎玖,每個 item 需要占滿一個屏幕或者更大,可以使用該部件生成列表笛粘,但是如果 item 的高度小于一個屏幕高度趁怔,那就不太推薦了,在首尾會用空白 item 來把未填滿的補上薪前,就是首尾都會留空白润努。我們使用 SliverFillViewportSliverFillRemaning 進行替換

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))),
          ),
    ]));
  }
}

最后的效果圖

header.gif

當然甜橱,為了方便擴展,需要重新封裝下 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 中使用攜帶 TabBarSliverAppBar(就是使用 SliverAppBarbottom 屬性添加 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 主要用來控制 SliverAppBarforceElevated 屬性幸海,當內部內容滾動時,顯示 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:這個方法返回的值對于 SliverOverlapAbsorberSliverOverlapInjector 部件是非常重要的參數(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 不能空,可以通過 NestedScrollViewsliverOverlapAbsorberHandleFor 方法來賦值溢谤,并且 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 部件,這個 handleSliverOverlapAbsorberhandle 保持一致即可阀参。

分析完源碼后肝集,例子的目標很明確,使用 SliverAppBar + TabBar + TabBarView蛛壳,先看下最后的效果圖吧

nested.gif
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ū)別捞挥,就是多了 SliverOverlapAbsorberSliverOverlapInjector 沒啥難度

以上部分代碼查看 nested_scroll_main.dart 文件

sliver 部件常用的也就那么多了,望小伙伴好好吸收忧吟,跟著例子擼擼代碼砌函,擼順下思路

最后代碼的地址還是要的:

  1. 文章中涉及的代碼:demos

  2. 基于郭神 cool weather 接口的一個項目,實現(xiàn) BLoC 模式溜族,實現(xiàn)狀態(tài)管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規(guī)范的讹俊,代碼更新會比較慢,雖然是跟著課上的一些寫代碼煌抒,但是還是做了自己的修改仍劈,很多地方看著不舒服,然后就改成自己的實現(xiàn)方式了):flutter_shop

如果對你有幫助的話寡壮,記得給個 Star贩疙,先謝過,你的認可就是支持我繼續(xù)寫下去的動力~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末诬像,一起剝皮案震驚了整個濱河市屋群,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坏挠,老刑警劉巖芍躏,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異降狠,居然都是意外死亡对竣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門榜配,熙熙樓的掌柜王于貴愁眉苦臉地迎上來否纬,“玉大人,你說我怎么就攤上這事蛋褥×偃迹” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長膜廊。 經(jīng)常有香客問我乏沸,道長,這世上最難降的妖魔是什么爪瓜? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任蹬跃,我火速辦了婚禮,結果婚禮上铆铆,老公的妹妹穿的比我還像新娘蝶缀。我一直安慰自己,他們只是感情好薄货,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布翁都。 她就那樣靜靜地躺著,像睡著了一般菲驴。 火紅的嫁衣襯著肌膚如雪荐吵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天赊瞬,我揣著相機與錄音,去河邊找鬼贼涩。 笑死巧涧,一個胖子當著我的面吹牛,可吹牛的內容都是我干的遥倦。 我是一名探鬼主播谤绳,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼袒哥!你這毒婦竟也來了缩筛?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤堡称,失蹤者是張志新(化名)和其女友劉穎瞎抛,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體却紧,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡桐臊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晓殊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片断凶。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖巫俺,靈堂內的尸體忽然破棺而出认烁,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布却嗡,位于F島的核電站舶沛,受9級特大地震影響,放射性物質發(fā)生泄漏稽穆。R本人自食惡果不足惜冠王,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舌镶。 院中可真熱鬧柱彻,春花似錦、人聲如沸餐胀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽否灾。三九已至卖擅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間墨技,已是汗流浹背惩阶。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扣汪,地道東北人断楷。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像崭别,于是被迫代替她去往敵國和親冬筒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容