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

該文已授權(quán)公眾號(hào) 「碼個(gè)蛋」,轉(zhuǎn)載請(qǐng)指明出處

上節(jié)最后留了個(gè)坑到這節(jié)來(lái)解決,因?yàn)樯婕安考容^多,所以留到這邊來(lái)繼續(xù)講,不然寫太多了怕小伙伴看不下去

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

SliverAppBar

相信很多 Android 開發(fā)的小伙伴會(huì)用到 MaterialDesignCollapsingToolbarLayout 來(lái)實(shí)現(xiàn)折疊頭部,既然 Android 有的,那么 Flutter 也不會(huì)少,畢竟 Flutter 主打的也是 MaterialDesign 啊。首先看下 SliverAppBar 的源碼吧,其實(shí)和 AppBar 的參數(shù)差不多,只是多了一些比較特殊的屬性

const SliverAppBar({
    Key key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace, // 通過(guò)這個(gè)來(lái)設(shè)置背景
    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, // 展開的高度
    // 以下三個(gè)等例子再講
    this.floating = false, 
    this.pinned = false,
    this.snap = false,
  })

別的參數(shù)應(yīng)該不陌生吧,都是 AppBar 的,那么直接來(lái)看個(gè)例子吧,還是通過(guò)上節(jié)說(shuō)的 CustomScrollView 來(lái)包裹 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,
        // 強(qiáng)制顯示陰影
        forceElevated: true,
        // 設(shè)置該屬性,當(dāng)有下滑手勢(shì)的時(shí)候,就會(huì)顯示 AppBar
//        floating: true,
        // 該屬性只有在 floating 為 true 的情況下使用,不然會(huì)報(bào)錯(cuò)
        // 當(dāng)上滑到一定的比例,會(huì)自動(dòng)把 AppBar 收縮(不知道是不是 bug,當(dāng) AppBar 下面的部件沒(méi)有被 AppBar 覆蓋的時(shí)候,不會(huì)自動(dòng)收縮)
        // 當(dāng)下滑到一定比例,會(huì)自動(dòng)把 AppBar 展開
//        snap: true,
        // 設(shè)置該屬性使 Appbar 折疊后不消失
//        pinned: true,
        // 通過(guò)這個(gè)屬性設(shè)置 AppBar 的背景
        flexibleSpace: FlexibleSpaceBar(
//          title: Text('Expanded Title'),
          // 背景折疊動(dòng)畫
          collapseMode: CollapseMode.parallax,
          background: Image.asset('images/timg.jpg', fit: BoxFit.cover),
        ),
      ),

      // 這個(gè)部件一般用于最后填充用的,會(huì)占有一個(gè)屏幕的高度,
      // 可以在 child 屬性加入需要展示的部件
      SliverFillRemaining(
        child: Center(child: Text('FillRemaining', style: TextStyle(fontSize: 30.0))),
      ),
    ]));
  }
}

這里分別給出不同的動(dòng)圖來(lái)查看三個(gè)屬性的影響

如果設(shè)置了 floating 屬性,當(dāng)有下拉動(dòng)作時(shí),會(huì)顯示 AppBar

floating.gif

如果設(shè)置了 snap 屬性,滑動(dòng)距離達(dá)到一定值后,會(huì)根據(jù)滑動(dòng)方向收縮或者展開

snap.gif

如果設(shè)置了 pinned 屬性,那么 AppBar 就會(huì)在界面上不會(huì)消失

pinned.gif

以上的效果圖把 SliverFillRemaining 換成列表 SliverFixedExtentList 效果可能會(huì)更加明顯,這邊給小伙伴自己替換測(cè)試吧。

SliverFillViewport

這邊提到了 SliverFillRemaining 用來(lái)填充視圖,那么順帶提下 SliverFillViewport 這個(gè)部件

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

如果一個(gè)滑動(dòng)列表,每個(gè) item 需要占滿一個(gè)屏幕或者更大,可以使用該部件生成列表,但是如果 item 的高度小于一個(gè)屏幕高度,那就不太推薦了,在首尾會(huì)用空白 item 來(lái)把未填滿的補(bǔ)上,就是首尾都會(huì)留空白。我們使用 SliverFillViewport 對(duì) SliverFillRemaning 進(jìn)行替換

SliverFillViewport(
          viewportFraction: 1.0,
          delegate: SliverChildBuilderDelegate(
              (_, index) => Container(child: Text('Item $index'), alignment: Alignment.center, color: colors[index % 4]),
              childCount: 10))

效果就不展示了,可自行運(yùn)行查看。

SliverToBoxAdapter

還記得上節(jié)最后的代碼中,有使用 SliverToBoxAdapter 這個(gè)部件嗎,這個(gè)部件只需要傳入一個(gè) child 屬性。因?yàn)樵?CustomScrollView 中只允許傳入 Sliver 部件,那么類似 Container 等普通部件就不可以使用了,那么這樣就需要更多的 Sliver 組件才能完成視圖,所以為了方便,直接通過(guò) SliverToBoxAdapter 對(duì)普通部件進(jìn)行包裹,這樣就成為一個(gè) Sliver 部件了。總結(jié)下 SliverToBoxAdapter 的功能就是 把一個(gè)普通部件包裹成為 Sliver 部件,例子就不舉了,上節(jié)已經(jīng)有了。

SliverPadding

那么在 CustomScrollView 中部件之間如何設(shè)置間距呢,可能你會(huì)想到用 SliverToBoxAdapter 包裹一個(gè) Padding 來(lái)處理,當(dāng)然沒(méi)問(wèn)題。不過(guò) Flutter 也提供了專門的部件 SliverPadding 使用方式同 Padding,但是需要傳入一個(gè) sliver 作為子類。

SliverPersistentHeader

Flutter 中,為我們提供了這么一個(gè)作為頭部的部件 SliverPersistentHeader,這個(gè)部件可以根據(jù)滾動(dòng)的距離縮小高度,有點(diǎn)類似 SliverAppBar 的背景效果。

const SliverPersistentHeader({
    Key key,
    @required this.delegate, // SliverPersistentHeaderDelegate,用來(lái)創(chuàng)建展示內(nèi)容
    this.pinned = false, // 同 SliverAppBar 屬性
    this.floating = false,
  }) 
SliverPersistentHeaderDelegate

這個(gè)代理比較特殊,是個(gè)抽象類,也就是需要我們自己進(jìn)行繼承后再實(shí)現(xiàn)方法。SliverPersistentHeaderDelegate 需要提供一個(gè)最大值,最小值,展示內(nèi)容,以及更新部件條件

比如我們需要展示一個(gè)最大高度 300,最小高度 100,居中的文字,那么我們可以這么寫這個(gè)代理類

class DemoHeader extends SliverPersistentHeaderDelegate {
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
        color: Colors.pink,
        alignment: Alignment.center,
        child: Text('我是一個(gè)頭部部件', style: TextStyle(color: Colors.white, fontSize: 30.0)));
  } // 頭部展示內(nèi)容

  @override
  double get maxExtent => 300.0; // 最大高度

  @override
  double get minExtent => 100.0; // 最小高度

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false; // 因?yàn)樗械膬?nèi)容都是固定的,所以不需要更新
}

使用 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),

      // 這個(gè)部件一般用于最后填充用的,會(huì)占有一個(gè)屏幕的高度,
      // 可以在 child 屬性加入需要展示的部件
          SliverFillRemaining(
            child: Center(child: Text('FillRemaining', style: TextStyle(fontSize: 30.0))),
          ),
    ]));
  }
}

最后的效果圖

header.gif

當(dāng)然,為了方便擴(kuò)展,需要重新封裝下 Delegate ,通過(guò)外部傳入范圍和展示內(nèi)容

// 自定義 SliverPersistentHeaderDelegate
class CustomSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double max; // 最大高度
  final double min; // 最小高度
  final Widget child; // 需要展示的內(nèi)容

  CustomSliverPersistentHeaderDelegate({@required this.max, @required this.min, @required this.child})
      // 如果 assert 內(nèi)部條件不成立,會(huì)報(bào)錯(cuò)
      : assert(max != null),
        assert(min != null),
        assert(child != null),
        assert(min <= max),
        super();

  // 返回展示的內(nèi)容,如果內(nèi)容固定可以直接在這定義,如果需要可擴(kuò)展,這邊通過(guò)傳入值來(lái)定義
  @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) {
    // 是否需要更新,這里我們定義當(dāng)高度范圍和展示內(nèi)容被替換的時(shí)候進(jìn)行刷新界面
    return max != oldDelegate.max || min != oldDelegate.min || child != oldDelegate.child;
  }
}

然后我們就可以愉快的使用了,不需要每個(gè) Delegate 都重新寫一遍,例如替換下剛才寫死的 DemoHeader

SliverPersistentHeader(
        // 屬性同 SliverAppBar
        pinned: true,
        floating: true,
        // 因?yàn)?SliverPersistentHeaderDelegate 是一個(gè)抽象類,所以需要自定義
        delegate: CustomSliverPersistentHeaderDelegate(
            max: 300.0, min: 100.0, child: Text('我是一個(gè)頭部部件', 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:一個(gè)內(nèi)部能夠嵌套其他滾動(dòng)部件,并使其滾動(dòng)位置聯(lián)結(jié)到一起的滾動(dòng)部件

/// 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 來(lái)展示 Tab 頁(yè)的內(nèi)容,這樣通過(guò)切換 Tab 頁(yè)就能展示該頁(yè)下的展示內(nèi)容。

看下 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:用于構(gòu)建 NestScrollView 的頭部部件,innerBoxIsScrolled 主要用來(lái)控制 SliverAppBarforceElevated 屬性,當(dāng)內(nèi)部?jī)?nèi)容滾動(dòng)時(shí),顯示 SliverAppbar 的陰影,主要用來(lái)提醒內(nèi)部的內(nèi)容低于 SliverAppBar (相當(dāng)于給人一種物理層次感,否則很容易被認(rèn)為,頭部和內(nèi)容是連接在一起的)

接下來(lái)看下 NestedScrollView 內(nèi)部個(gè)人覺(jué)得有點(diǎn)重要的一個(gè)方法 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;
}

請(qǐng)注意到中間的注釋

糟透了的翻譯 X 4:這個(gè)方法返回的值對(duì)于 SliverOverlapAbsorberSliverOverlapInjector 部件是非常重要的參數(shù)

接著請(qǐng)注意代碼中的那段 assert 中的文字

糟透了的翻譯 X 5:sliverOverlapAbsorberHandleFor 傳入的參數(shù) context 中必須包含 NestedScrollView

SliverOverlapAbsorber

這邊又引入了兩個(gè)部件 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:一個(gè) sliver 部件,用于把部件重疊的高度反饋給 SliverOverlapAbsorberHandle,而且指明了 handle 不能空,可以通過(guò) NestedScrollViewsliverOverlapAbsorberHandleFor 方法來(lái)賦值,并且 child 必須是個(gè) sliver 部件,也就是說(shuō)我們的 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)建一個(gè)和指定的 handle 一樣高度的 sliver 部件,這個(gè) handleSliverOverlapAbsorberhandle 保持一致即可。

分析完源碼后,例子的目標(biāo)很明確,使用 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 值,直接通過(guò) `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
                      // 這邊需要通過(guò) Builder 來(lái)創(chuàng)建 TabBarView 的內(nèi)容,否則會(huì)報(bào)錯(cuò)
                      // 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` 重疊部分頂出來(lái),否則會(huì)被遮擋
                                    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()))),
    );
  }
}

使用的部件和之前講的沒(méi)啥大區(qū)別,就是多了 SliverOverlapAbsorberSliverOverlapInjector 沒(méi)啥難度

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

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

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

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

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

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

如果對(duì)你有幫助的話,記得給個(gè) Star,先謝過(guò),你的認(rèn)可就是支持我繼續(xù)寫下去的動(dòng)力~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容