flutter 效果实现 —— NestedScrollView 嵌套滚动(多固定头)

效果

有点类似 flexibleSpace,但是 flexibleSpace 的 expandedHeight 得预留计算出来。这里的头图的大小可以自适应,不用显式设置 expandedHeight。

注:最新版本可以使用 SliverMainAxisGroup 实现相关需求(存疑?)

代码

注:请添加依赖 sliver_tools

class MultiPinNestedTabsPage extends StatelessWidget {
  MultiPinNestedTabsPage({Key? key}) : super(key: key);

  final List<String> tabs = ["水果", "蔬菜"];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        backgroundColor: Colors.blueGrey,
        body: NestedScrollView(
          physics: BouncingScrollPhysics(),
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              SliverOverlapAbsorber(
                //关键
                handle:
                    NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                sliver: MultiSliver(
                  // pushPinnedChildren: true,
                  children: [
                    SliverAppBar(
                      backgroundColor: Colors.transparent,
                      pinned: true,
                      title: Text("固定导航"),
                    ),
                    SliverToBoxAdapter(
                      child: Container(
                        color: Colors.pink,
                        height: 200,
                        alignment: Alignment.center,
                        child: Text("头图"),
                      ),
                    ),
                    // 可替换成 SliverPinnedHeader
                    SliverAppBar(
                      primary: false,
                      toolbarHeight: 0,
                      pinned: true,
                      backgroundColor: Colors.transparent,
                      bottom: TabBar(
                        tabs: tabs
                            .map((e) => Tab(
                                  text: e,
                                ))
                            .toList(),
                      ),
                    ),
                  ],
                ),
              ),
            ];
          },
          body: TabBarView(
            children: [
              Builder(builder: (context) {
                return CustomScrollView(
                  slivers: [
                    // 关键
                    SliverOverlapInjector(
                      handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                          context),
                    ),
                    SliverToBoxAdapter(
                      child: Text("Tab1"),
                    )
                  ],
                );
              }),
              Builder(builder: (context) {
                return CustomScrollView(
                  slivers: [
                    SliverToBoxAdapter(
                      child: Text("Tab2"),
                    )
                  ],
                );
              }),
            ],
          ),
        ),
      ),
    );
  }
}

有问题的版本 1

由于使用了 SliverFillRemaining,列表向上拉的时候将占据整个屏幕,而未停在固定头的底部。此外,由于只用了单个 viewport,因此滚动区域的起始位置(physics 区域)始终位于顶部。

class MultiPinTabsPage extends StatelessWidget {
  MultiPinTabsPage({Key? key}) : super(key: key);

  final List<String> tabs = ["水果", "蔬菜"];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        backgroundColor: Colors.blueGrey,
        // 创建一个公共的 Scrollable 和 Viewport
        body: CustomScrollView(
          // physics: BouncingScrollPhysics(),
          slivers: [
            SliverAppBar(
              backgroundColor: Colors.transparent,
              pinned: true,
              title: Text("固定导航"),
            ),
            SliverToBoxAdapter(
              child: Container(
                color: Colors.pink,
                height: 200,
                alignment: Alignment.center,
                child: Text("头图"),
              ),
            ),
            SliverAppBar(
              elevation: 0,
              primary: false,
              toolbarHeight: 0,
              pinned: true,
              backgroundColor: Colors.transparent,
              bottom: TabBar(
                tabs: tabs
                    .map((e) => Tab(
                          text: e,
                        ))
                    .toList(),
              ),
            ),
            SliverFillRemaining(
              // 向上滚动会占据整个屏幕
              child: TabBarView(
                children: [
                  Text("Tab1"),
                  Text("Tab2"),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

有问题的版本 2

使用了 NestedScrollView 控制同步多列表的同步滚动。问题同版本1,即向上拉的时候直接拉到顶部,而未在停在 PinnedSliver 的底部。这是因为缺少沟通机制(SliverOverlapAbsorber),下方列表并不知道上方固定的部分占据多大的空间。

class SliverHeaderNestedTabsPage extends StatelessWidget {
  SliverHeaderNestedTabsPage({Key? key}) : super(key: key);

  final List<String> tabs = ["水果", "蔬菜"];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        backgroundColor: Colors.blueGrey,
        body: NestedScrollView(
          physics: BouncingScrollPhysics(),
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              SliverAppBar(
                backgroundColor: Colors.transparent,
                pinned: true,
                title: Text("固定导航"),
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
                  return Column(
                    children: <Widget>[
                      Container(
                        color: Colors.pink,
                        height: 200,
                        alignment: Alignment.center,
                        child: Text("头图"),
                      )
                    ],
                  );
                }, childCount: 1),
              ),
              // 这里可替换成 SliverPinnedHeader 或 SliverAppBar
              SliverPersistentHeader(
                pinned: true,
                delegate: ContentTabHeader(
                  child: TabBar(
                    tabs: tabs
                        .map((e) => Tab(
                              text: e,
                            ))
                        .toList(),
                  ),
                ),
              ),
            ];
          },
          body: TabBarView(
            children: [
              Builder(builder: (context) {
                return CustomScrollView(
                  slivers: [
                    SliverToBoxAdapter(
                      child: Text("Tab1"),
                    )
                  ],
                );
              }),
              Builder(builder: (context) {
                return CustomScrollView(
                  slivers: [
                    SliverToBoxAdapter(
                      child: Text("Tab2"),
                    )
                  ],
                );
              }),
            ],
          ),
        ),
      ),
    );
  }
}

class ContentTabHeader extends SliverPersistentHeaderDelegate {
  const ContentTabHeader({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return child;
  }

  @override
  double get maxExtent => kTextTabBarHeight;

  @override
  double get minExtent => kTextTabBarHeight;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return false;
  }
}

posted on 2022-08-15 22:39  Lemo_wd  阅读(3906)  评论(0编辑  收藏  举报

导航