Flutter学习 可滚动Widget 下

12. 嵌套可滚动组件 NestedScrollView

之前说过, 如果 CustomScrollView 只能组合 Sliver ,并且依赖子组件的滑动方向不一致,如果滑动方向一致时,则不能正常工作,为了解决这个问题, Flutter 中提供了一个 NestedScrollView,它的功能是组合两个滚动组件,下面来看看其定义:

class NestedScrollView extends StatefulWidget {
    
    
  const NestedScrollView({
    
    
    ...
    //通用属性已省略
    // header, sliver构造器
    required this.headerSliverBuilder,
    // 可以接受任意的滚动组件
    required this.body,
    this.dragStartBehavior = DragStartBehavior.start,
    this.floatHeaderSlivers = false,
  })

12.1 示例

我们这里来实现一个页面,功能点如下:

  1. 最上面是一个 AppBar, 用于导航,和固定在顶部
  2. AppBar 下面是一个 SliveList, 可以有5个列表项组成
  3. 最下面是一个 ListView

预期效果是 SliverList 和下面的 ListView 的滑动能够统一,而不是下面ListView上滑动时只有ListView能够响应滑动,整一个页面在垂直方向是一体的,代码实现如下:

class _NestedScrollViewRouteState extends State<NestedScrollViewRoute> {
    
    
  @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
    
    
          return [
            SliverAppBar(
              title: const Text("嵌套 ListView"),
              pinned: true,
              forceElevated: innerBoxIsScrolled,
            ),
            buildSliverList(5),
          ];
        },
        body: ListView.builder(
            padding: const EdgeInsets.all(8.0),
            physics: const ClampingScrollPhysics(),
            itemCount: 30,
            itemBuilder: (context, index) {
    
    
              return SizedBox(
                  height: 50, child: Center(child: Text("Item $index")));
            }),
      ),
    );
  }

  Widget buildSliverList([int count = 5]) {
    
    
    return SliverFixedExtentList(
      itemExtent: 50,
      delegate: SliverChildBuilderDelegate(
            (context, index) {
    
    
          return ListTile(title: Text('$index'));
        },
        childCount: count,
      ),
    );
  }
}

在这里插入图片描述
NestedScrollView 在逻辑上将可滚动组件分成了 header 和 body 两个部分, header 可以认为是外部可滚动组件,可以认为这个可滚动组件就是 CustomScrolView, 所以它只能接收 Sliver, 通过 headerSliverBuilder 来构建一个 Sliver 列表给外部的可滚动组件, 而 body 部分可以接受任意可滚动组件, 该可滚动组件成为 内部可滚动组件

12.2 NestedScrollView 原理

来看看下图:
在这里插入图片描述

  1. NestedScrollView 是 CustomScrollView 的子类
  2. header 和body 都是 CustomScrollView 的子 Sliver
  3. 当 body 是一个可滚动组件时,它和 CustomScrollView 分别有一个 Scrollable, 由于 body 在 CustomScrollView 的内部,所以称其为内部可滚动组件,称 CustomScrollView 为外部可滚动组件,同时,因为 header 部分是 sliver, 所以没有独立的 Scrollable,可以称 header 为外部可滚动组件
  4. NestedScrollable 的核心就是通过一个协调器来协调外部可滚动组件和内部可滚动组件,以使滑动效果连贯统一,协调器的实现原理是分别给内外可滚动组件分别设置一个 controller, 然后这两个 controller 来控制它们的滚动

在使用 NestedScrollView 有两点需要注意:

  • 要确认 body 的 physics是否需要设置为 ClampingScrollPhysics,因为会影响 iOS 的体验。但是如果 header 中只有一个 AliverAppBar 则不应该天机,因为 SliverAppBar 是固定在顶部的
  • body 不能设置 controllerprimary,因为协调器已经默认帮其实现了, 如果重新设定,则协调器会失效

12.3 SliverAppBar

SliverAppBar 是 AppBar 的 Sliver版本, 常用的场景是作为 NestedScrollView 的 header, 下面是其构造函数:

  const SliverAppBar({
    
    
    // 收缩起来的高度
    this.collapsedHeight,
    // 展开时的高度
    this.expandedHeight,
    // 是否漂浮
    this.floating = false,
    this.pinned = false,
    // 当漂浮时, 该参数才有效
    this.snap = false,
    ...
  })
  • SliverAppbar 在 NestedScrollView 中随着用户的滑动是可以收缩和展开的,因此我们需要分别指定收缩和展开的高度
  • pinned 为 true 时 SliverAppBar 会固定在 NestedScrollView 的顶部
  • floatingsnap
    floating 为 true 时, SliverAppBar 不会固定到顶部,当用户向上滑动到顶部时,SliverAppBar 也会滑出可视窗口,当用户反向滑动时,SliverAppBar 的 snap 为 true, 此时无论 SliverAppBar 滑出屏幕多远,都会立即回到屏幕顶部, 如果 snap 为false,只有向下滑动到边界时才会重新回到屏幕顶部

下面来看一个示例:

 @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
    
    
          return [
            // 实现 snap 效果
            SliverAppBar(
              floating: true,
              snap: true,
              expandedHeight: 200,
              forceElevated: innerBoxIsScrolled,
              flexibleSpace: FlexibleSpaceBar(
                  background:
                      Image.asset("images/bobo.jpg", fit: BoxFit.cover)),
            )
          ];
        },
        body: Builder(builder: (context) {
    
    
          return CustomScrollView(
            slivers: [buildSliverList(100)],
          );
        }),
      ),
    );
  }

初始状态:
在这里插入图片描述
滑动到顶部:
在这里插入图片描述
滑动到顶部后轻微向下滑动:
在这里插入图片描述
当我们滑动到顶部时,然后反向轻微滑动一点,这个时候 SliverAppBar 就会整体回到屏幕顶部,但这时有一个问题,就是图中红色的部分,发现 SlvierAppBar 回到屏幕后 0-4 这几个列表项遮住了, 是不符合预期的,因为往下滑动时,用户就是为了看到上面的内容,这部分内容正好被 SliverAppBar 遮住了,这样体验就很不好。

为了解决这个问题, 立马想到的思路就是当 SliverAppBar 在回到屏幕的过程中,底下的列表项也同时往下滑相同的偏移量。但是监听的 controller 是 NestedScrollView ,我们很难获取, 就算通过 context 获取, 这也会侵入到 NestedScrollView 的逻辑,不符合职责分离原则。

Flutter 也意识到了这一点,所以提供了解决方案,来看看修改后的代码:

  @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
    
    
          return [
            // 实现 snap 效果
            SliverOverlapAbsorber(
              handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
              sliver: SliverAppBar(
                floating: true,
                snap: true,
                expandedHeight: 200,
                forceElevated: innerBoxIsScrolled,
                flexibleSpace: FlexibleSpaceBar(
                    background:
                        Image.asset("images/bobo.jpg", fit: BoxFit.cover)),
              ),
            )
          ];
        },
        body: Builder(builder: (context) {
    
    
          return CustomScrollView(
            slivers: [
              SliverOverlapInjector(
                  handle:
                      NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
              buildSliverList(100)
            ],
          );
        }),
      ),
    );
  }
  1. SliverAppBar 使用了 SliverOverlapAbsorber 包裹了起来, 它的作用就是获取 SlvierAppBar 返回时遮住内部可滚动组件部分的长度,这个长度就是 overlap 的长度
  2. 在 body 中往 CustomScrollView 的 Sliver 列表最前面插入了一个 SliverOverlapInjector ,它会将 SliverOverlapAbsorber 中获取的 overlap 长度应用到内部可滚动组件中,这样 SliverAppBar 返回时内部可滚动组件也会相应的同步滑动相应的距离

这两个新玩意都接收一个 handle, 给它传入的是 NestedScrollView.sliverOverlapAbsorberHandleFor(context), 这个就是 SliverOverlapAbsorber 和 SliverlapInjector 的通信桥梁,即 overlap 长度

这就是Flutter提供的解决方案,或许有点不太优雅,但目前也没有更好的解决方案

12.4 嵌套 TabBarView

实现商城主页,它有三个 Tab,为了获得更大的商品显示空间, 我们希望用户向上滑动时,导航栏能够滑出屏幕,当用户向下滑动时,导航栏能够快速回到屏幕,因为向下滑动时可能是用户向看之前的商品,也可能是用户想找到导航栏返回,代码如下:

  @override
  Widget build(BuildContext context) {
    
    
    final _tabs = ["消息", "发现", "我的"];
    return DefaultTabController(length: _tabs.length, child: Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
    
    
          return [
            SliverOverlapAbsorber(handle: NestedScrollView
                .sliverOverlapAbsorberHandleFor(context),
                sliver: SliverAppBar(
                  title: Text("商城"),
                  floating: true,
                  snap: true,
                  forceElevated: innerBoxIsScrolled,
                  bottom: TabBar(
                    tabs: _tabs.map((name) => Tab(text: name)).toList(),
                  ),
                )),
          ];
        },
        body: TabBarView(
          children: _tabs.map((String name) {
    
    
            return Builder(
              builder: (BuildContext context) {
    
    
                return CustomScrollView(
                  key: PageStorageKey<String>(name),
                  slivers: [
                    SliverOverlapInjector(
                        handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                            context)),
                    SliverPadding(padding: const EdgeInsets.all(10.0),
                      sliver: buildSliverList(50),),
                  ],
                );
              },
            );
          }).toList(),
        ),
      ),
    ));
  }

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/122054396