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

該文已受權公衆號 「碼個蛋」,轉載請指明出處git

上節最後留了個坑到這節來解決,由於涉及部件比較多,因此留到這邊來繼續講,否則寫太多了怕小夥伴看不下去github

在上節最後,給小夥伴們展現了 SliveGridSliverFixedExtentList 的用法,基本上和 GridViewListView 的用法差很少,因此這邊就很少講這兩個部件了。markdown

SliverAppBar

相信不少 Android 開發的小夥伴會用到 MaterialDesignCollapsingToolbarLayout 來實現摺疊頭部,既然 Android 有的,那麼 Flutter 也不會少,畢竟 Flutter 主打的也是 MaterialDesign 啊。首先看下 SliverAppBar 的源碼吧,其實和 AppBar 的參數差很少,只是多了一些比較特殊的屬性app

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,
  })
複製代碼

別的參數應該不陌生吧,都是 AppBar 的,那麼直接來看個例子吧,仍是經過上節說的 CustomScrollView 來包裹 Sliver 部件less

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))),
      ),
    ]));
  }
}
複製代碼

這裏分別給出不一樣的動圖來查看三個屬性的影響ide

若是設置了 floating 屬性,當有下拉動做時,會顯示 AppBar測試

floating.gif

若是設置了 snap 屬性,滑動距離達到必定值後,會根據滑動方向收縮或者展開flex

snap.gif

若是設置了 pinned 屬性,那麼 AppBar 就會在界面上不會消失動畫

pinned.gif

以上的效果圖把 SliverFillRemaining 換成列表 SliverFixedExtentList 效果可能會更加明顯,這邊給小夥伴本身替換測試吧。ui

####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

還記得上節最後的代碼中,有使用 SliverToBoxAdapter 這個部件嗎,這個部件只須要傳入一個 child 屬性。由於在 CustomScrollView 中只容許傳入 Sliver 部件,那麼相似 Container 等普通部件就不可使用了,那麼這樣就須要更多的 Sliver 組件才能完成視圖,因此爲了方便,直接經過 SliverToBoxAdapter 對普通部件進行包裹,這樣就成爲一個 Sliver 部件了。總結下 SliverToBoxAdapter 的功能就是 把一個普通部件包裹成爲 Sliver 部件,例子就不舉了,上節已經有了。

SliverPadding

那麼在 CustomScrollView 中部件之間如何設置間距呢,可能你會想到用 SliverToBoxAdapter 包裹一個 Padding 來處理,固然沒問題。不過 Flutter 也提供了專門的部件 SliverPadding 使用方式同 Padding,可是須要傳入一個 sliver 做爲子類。

SliverPersistentHeader

Flutter 中,爲咱們提供了這麼一個做爲頭部的部件 SliverPersistentHeader,這個部件能夠根據滾動的距離縮小高度,有點相似 SliverAppBar 的背景效果。

const SliverPersistentHeader({
    Key key,
    @required this.delegate, // SliverPersistentHeaderDelegate,用來建立展現內容
    this.pinned = false, // 同 SliverAppBar 屬性
    this.floating = false,
  }) 
複製代碼
SliverPersistentHeaderDelegate

這個代理比較特殊,是個抽象類,也就是須要咱們本身進行繼承後再實現方法。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:一個內部可以嵌套其餘滾動部件,並使其滾動位置聯結到一塊兒的滾動部件

/// 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 部件是很是重要的參數

接着請注意代碼中的那段 assert 中的文字

糟透了的翻譯 X 5:sliverOverlapAbsorberHandleFor 傳入的參數 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:建立一個和指定的 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 來建立 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()))),
    );
  }
}
複製代碼

使用的部件和以前講的沒啥大區別,就是多了 SliverOverlapAbsorberSliverOverlapInjector 沒啥難度

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

sliver 部件經常使用的也就那麼多了,望小夥伴好好吸取,跟着例子擼擼代碼,擼順下思路

最後代碼的地址仍是要的:

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

  2. 基於郭神 cool weather 接口的一個項目,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規範的,代碼更新會比較慢,雖然是跟着課上的一些寫代碼,可是仍是作了本身的修改,不少地方看着不舒服,而後就改爲本身的實現方式了):flutter_shop

若是對你有幫助的話,記得給個 Star,先謝過,你的承認就是支持我繼續寫下去的動力~

相關文章
相關標籤/搜索