flutter-Sliver

1,035 阅读13分钟

前言

我们平时用的 ListView、GridView,看着很够用,但碰到比较较为复杂的功能就显得,例如:淘宝首页这种,因此我们引出了 Sliver,都是可以说滚动视图的尽头是 Sliver,包括ListView、GridView你也可以从他们身上看到Sliver的影子

下面就介绍一下较为常用的 Sliver,话不多说,先上一张美图

six_wings_angle_gril.png

案例demo(sliver_view目录)

Sliver简介

前面说了滚动视图的尽头是 Sliver,可以看到其乃万金油,包裹 ListView、GridView在里面都有替代品,下面先来个三问: Sliver 是什么,里面有什么,可以用来干什么

Sliver 也是一个支持滚动嵌套的 Widget,里面处理了普通滚动视图嵌套一些滚动组件后的冲突问题 (不信,先嵌套一个 Tabbar 试试),使用里面特有的滚动视图,可以无缝嵌套做成我们想要的效果,并且还有一些自带的吸顶等效果方便我们使用

同时 Sliver一旦我们使用习惯了,发现其么不是那么神奇了,即使我们还没读源码,就能解决大部分功能了(还有一些特殊的功能可能仍需要更好的解决方案,如果自己无法解决,可以采用第三方来实现)

Sliver默认使用 CustomScrollView作为底部基础滚动视图,子组件必须是 Sliver 系列的组件,否则会报错(NestedScrollView稍微不一样,虽然和CustomScrollView功能类似,但额外做了一些操作,某些场景使用更方便了一些)

常用的Sliver系列组件有:SliverToBoxAdapter、SliverList、SliverFixedExtentList、SliverGrid、SliverGrid.extent、SliverGrid.count、SliverAppBar、SliverPersistentHeaderSliverSafeAreaSliverFillRemaining等,后面会有案例介绍

SliverToBoxAdapter这里提前介绍,由于Sliver里面只能使用 Sliver系列组件,因此在使用其他自定义组件时需要转化,就是通过该组件,使用如下所示

SliverToBoxAdapter(
  child: Container(),//放入自定义组件即可
);

SliverSafeArea(和SafeArea一样)、SliverFillRemaining(填充剩余屏幕空间)

ps:为了方便切换展示查看,Sliver 系列的用 TabBar 把他们放到了一页,因此每一个标签下的才是我们的效果图

SliverList-ListView

先看一小效果图,下面是通过几个SliverList与一个Text文本组成的长列表

image.png

先生成一个可以在 Sliver 中使用的文本,使用 SliverToBoxAdapter 包裹起来即可

Widget getTitle(String title) {
    return SliverToBoxAdapter(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 10),
        child: Text(title, style: const TextStyle(color: Colors.black, fontSize: 20,), textAlign: TextAlign.center,),
      ),
    );
  }

SliverList

SliverChildListDelegate

SliverList 使用代理 SliverChildListDelegate,和ListView一样,生成多个子组件的列表

Widget getListViewWidget() {
  return SliverList(
    delegate: SliverChildListDelegate(<Widget>[
      Padding(
        padding: const EdgeInsets.all(10),
        child: Container(
          color: Colors.green,
          height: 100,
        ),
      ),
      Padding(
        padding: const EdgeInsets.all(10),
        child: Container(
          color: Colors.green,
          height: 100,
        ),
      ),
    ]),
  );
}

SliverChildBuilderDelegate

SliverList 使用代理 SliverChildBuilderDelegate,和ListView.builder一样,可以生成可以复用的 Builder,使用简单

//可复用的ListViewBuilder
Widget getListViewBuilder() {
  return SliverList(
    delegate: SliverChildBuilderDelegate((context, index) {
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 5),
        child: Container(
          margin: const EdgeInsets.symmetric(horizontal: 10),
          color: Colors.red,
          height: 20,
        ),
      );
    }, childCount: 10,),
  );
}

SliverFixedExtentList

SliverFixedExtentList其为固定行高的 SliverList语法糖,即所有 builder 高度都一样,内部不会根据内容高度动态变化,使用较少,由于行高固定,性能略高,需要优化时,可以可以根据情况考虑

//可复用的固定行高语法糖ListViewBuilder
Widget getListFixedExtentBuilderWidget() {
  return SliverFixedExtentList(
    delegate: SliverChildBuilderDelegate(
          (context, index) {
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 10),
          child: Container(
            margin: const EdgeInsets.symmetric(vertical: 5),
            color: Colors.blue,
          ),
        );
      },
      childCount: 10, //总数量
    ),
    itemExtent: 30, //固定行高
  );
}

SliverGrid-GridView

话不多说先上图 通过几个SliverGrid组成的列表,SliverToBoxAdaptertitle就不多说了

image.png

SliverGrid.count

SliverGrid.count为固定列数的语法糖,可放置多个子类,单个item宽度是根据行宽、间距、列数计算的出来的结果,

//固定列数的语法糖
Widget getGridCountWidget() {
  return SliverGrid.count(
    crossAxisCount: 6,
    mainAxisSpacing: 6,
    crossAxisSpacing: 6,
    childAspectRatio: 1, //宽高比,想高一点,就介于0~1之间即可
    children:
        [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4].map<Widget>((e) {
      return Container(
        color: Colors.red,
      );
    }).toList(),
  );
}

SliverGrid.extent

SliverGrid.extent为单个item最大行宽语法糖,在满足铺满整行的情况下,通过适当缩小或保持不变最大列宽,对整行剩余空间,以最少列数进行平分,一般用的比较少(如果想在pad一页显示更多内容,手机保持原样,这个布局是还是可以的)

更加详细案例,可以参考前面的 GridViewSliverGridDelegateWithMaxCrossAxisExtent 代理介绍

//单个item最大行宽语法糖
Widget getGridExtentWidget() {
  return SliverGrid.extent(
    maxCrossAxisExtent: 80,
    mainAxisSpacing: 6,
    crossAxisSpacing: 6,
    childAspectRatio: 1, //宽高比,想高一点,就介于0~1之间即可
    children:
        [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4].map<Widget>((e) {
      return Container(
        color: Colors.green,
      );
    }).toList(),
  );
}

SliverGrid-builder

和前面两个类似,这个是基于他们的可以复用的 builder

SliverGridDelegateWithFixedCrossAxisCount

SliverGrid.count的可复用 builder 版本,适用于优化长列表,需要设置 SliverGridDelegateWithFixedCrossAxisCount 代理

//复用builder固定列数
Widget getGridBuilderFixCrossWidget() {
  return SliverGrid(
    //固定列数
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 2,
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 1, //宽高比,想高一点,就介于0~1之间即可
    ),
    delegate: SliverChildBuilderDelegate(
          (context, index) {
        return Container(
          color: Colors.blue,
        );
      },
      childCount: 4,
    ),
  );
}

SliverGridDelegateWithMaxCrossAxisExtent

SliverGrid.extent的可复用 builder 版本,用于优化长列表,需要设置SliverGridDelegateWithMaxCrossAxisExtent代理,逻辑与其类似,需要单个item的最大列宽限制,用的好的话,对于多个平台的适配,也许会形成更好的展示方案

//单个item复用builder最大列宽
Widget getGridBuilderMaxCrossWidget() {
  return SliverGrid(
    //最大列宽
    gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 120,
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 1, //宽高比,想高一点,就介于0~1之间即可
    ),
    delegate: SliverChildBuilderDelegate(
          (context, index) {
        return Container(
          color: Colors.yellow,
        );
      },
      childCount: 4,
    ),
  );
}

SliverAppBar-AppBar

Sliver中的AppBar,应用到 Sliver中会有一些默认appbar与背景联动的效果(当然也可以在外部使用以前的固定AppBar),下面介绍一下 SliverAppBar

里面只要有三种效果,两种悬浮,一种吸顶效果

floating:默认悬浮效果,需要floating设置为true,向下滚动会逐渐隐藏顶部 Appbar,向上滚动会慢慢显示 Appbar

snap:默认悬浮效果,需要floating、snap同时设置为true, 向下滚动松手会立即隐藏整个伸开的Appbar,向上滚动会立即显示整个Appbar

pinned:默认吸顶效果,需要pinned设置为true,从顶部向下滚动逐渐缩小appbar至最小高度(背景消失,仅留下toolbar),从下面滚动到接近顶部时,会逐渐从扩展appbar至最大高度(背景和bar内容完全显示)

简单看一下效果(主要是 floating 的显隐,以及 pinned 缩小到 toolbar 高度样式)

image.png

SliverAppBar(
  //如果不是应用到主控制器,设置false,避免显示返回箭头,默认的AppBar也有
  automaticallyImplyLeading: false,
  floating: true,
  //snap: true,
  //pinned: true,
  //缩小的最小高度,不能低于toolbar的默认高度56(修改小了也不行)
  collapsedHeight: 56,
  //拉伸最大高度
  expandedHeight: 200,
  elevation: 0, //高度和appbar一样设置了有利于规避阴影
  //FlexibleSpaceBar类似于朋友圈背景墙的组件
  //title底部文字,background背景图片、centerTitle中间文字,根据自己需要定制即可
  flexibleSpace: FlexibleSpaceBar(
    title: TextButton(
      onPressed: () {
        status++;
        status %= 3;
        setState(() {});
      },
      child: Text(
        "点击切换Bar,当前类型: ${tabNames[status]}",
        style: const TextStyle(color: Colors.white, fontSize: 12),
      ),
    ),
    background: Container(
      color: Colors.blue,
      height: 150,
    ),
  ),
);

调用如下所示,只需要放到前面就可以了

CustomScrollView(
  slivers: <Widget>[
    //切换状态栏,这里简化后的调用方法,实际一般就一个SliverAppBar
    getSliverAppBar(),

    SliverFixedExtentList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          final isSingle = index % 2 == 1;
          return Container(
            alignment: Alignment.center,
            color: isSingle ? Colors.white : Colors.greenAccent,
            child: Text(
              index.toString(),
              style: const TextStyle(
                fontSize: 20,
                color: Colors.black,
              ),
            ),
          );
        },
        childCount: 50,
      ),
      itemExtent: 60,
    ),
  ],
),

PS:可以尝试使用多个 SliverAppBar,享受渐进式展开与合并的效果

Sliver-SliverPersistentHeader

SliverPersistentHeader是一个可以自定义类似于 SliverAppBar 效果的组件,定制的组件需要遵循SliverPersistentHeaderDelegate协议,可以自定义出更适合自己应用的效果

先看一下自定义的吸顶,效果图(也只比较常见的)

image.png

SliverPersistentHeader调用

调用起来和 SliverAppbar差不多,需要使用 SliverPersistentHeader,然后遵循SliverPersistentHeaderDelegate效果即可(我们需要自定义子类继承他),我们可以通过代理的偏移值,来动态调整我们内部组件色彩、组件等变更

//调用代码
CustomScrollView(
  slivers: [
    SliverPersistentHeader(
      pinned: true, //设置吸顶效果
      delegate: CeilStickyHeadDelegate(
        background: Image.asset("images/six_wings_angle_gril.png", fit: BoxFit.cover,),
        collapsedHeight: 50,
        expandHeight: 300,
        title: "六翼天使",
      ),
    ),
    ...
  ],
),

注意:传递的背景图片最好设置成 BoxFit.cover,否则可能不会占满屏幕,如果要想类似的缩放效果,expandHeight高度小于图片的高度即可,如果不想缩放效果,那么可以直接设置成一样高即可

自定义吸顶效果 SliverPersistentHeaderDelegate

自定义的时候需要继承自 SliverPersistentHeaderDelegate代理类,主要需要重写下面几个方法:minExtent、maxExtent、build、shouldRebuild

minExtent、maxExtent:分别代表最小高度toolbar高度,最大高度背景高度

shouldRebuild:是否支持重新构建,如果为false,即使外部更新属性,组件也不会被重新构建,根据需要设置即可

build:编写背景和toolbar的渐变效果使用,依赖于返回的 shrinkOffset 参数来进行我们的渐变操作

//自定义顶部效果(这里类似朋友圈),需要继承自SliverPersistentHeaderDelegate,这样定制起来更简洁
class CeilStickyHeadDelegate extends SliverPersistentHeaderDelegate {
  final Widget background;
  final double collapsedHeight;
  final double expandHeight;
  final Widget? titleView;

  CeilStickyHeadDelegate({
    required this.background,
    required this.collapsedHeight,
    required this.expandHeight,
    Widget? titleView,
    String? title,
  }): assert(titleView == null || title == null, '不能同时传递两个',),
      titleView = titleView ?? Text(title ?? '', style: const TextStyle(color: Colors.black, fontSize: 16,),);

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

  @override
  double get maxExtent => expandHeight; //最大高度 背景高度

  double opacity = 0;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox(
      height: maxExtent,
      child: Stack(
        fit: StackFit.expand, //内部铺满
        children: [
          background,
          Positioned(
            left: 0,
            top: 0,
            right: 0,
            //整个顶部内容效果,为了保证顶部statusbar也有效果
            child: Container(
              color: getBkgColor(shrinkOffset),
              child: SafeArea(
                //取消底部间距
                bottom: false,
                //这里就是 toolbar内容了,我们改变的只有这个
                child: SizedBox(
                  height: collapsedHeight,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      IconButton(
                        onPressed: () {
                          Navigator.pop(context);
                        },
                        icon: Icon(
                          Icons.arrow_back_ios_new,
                          color: getTextColor(shrinkOffset, true),
                        ),
                      ),
                      Opacity(
                        opacity: (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1),
                        child: titleView!,
                      ),
                      IconButton(
                        onPressed: () {
                          print("点击了分享");
                        },
                        icon: Icon(
                          Icons.share,
                          color: getTextColor(shrinkOffset, true),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Color getBkgColor(double shrinkOffset) {
    final double opacity = (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1);
    return Color.fromRGBO(0xff, 0xff, 0xff, opacity);
  }

  Color getTextColor(double shrinkOffset, bool isIcon) {
    if (shrinkOffset <= 50) {
      //偏移较小时,默认透明
      return isIcon ? Colors.white : Colors.transparent;
    } else {
      //下滑文字慢慢变成黑色,白底
      final double opacity = (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1);
      return Color.fromRGBO(0, 0, 0, opacity);
    }
  }

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    //是否支持重新构建,如果为false,即使外部更新属性,组件也不会被重新构建,根据需要设置即可
    return true;
  }
}

Sliver-NestedScrollView

类似于前面使用的 CustomScrollView自动帮我们整合头和体,系统也为我们准备了一个更好的组件 NestedScrollView,其将Header头和body分开,能让我们更好地使用,看着更清晰,除了头部需要Sliver系列body可以使用日常组件

下面就是用默认的 SliverAppBarBuilder 试试效果

//需要注意的是 Sliver 对于有限滚动内容的滑动是连贯的,可以拉伸,一旦出现builder,则拉伸会出现分离现象
//如果需要连贯,可以采用一个Builder即可
//另外,新起一个页面时,受安全区域影响,滚动视图默认会有一个内部padding,可以通过设置 padding来解除
class NestedScrollNormalView extends StatelessWidget {
  const NestedScrollNormalView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return <Widget>[
            //上面使用一个SliverAppBar
            SliverAppBar(
              expandedHeight: 300.0,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text("六翼天使",
                  style: TextStyle(color: Colors.white, fontSize: 16),),
                background: Image.asset(
                  "images/six_wings_angle_gril.png",
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ];
        },
        body: ListView.builder(
          padding: const EdgeInsets.only(top: 10), //设置padding,避免默认的Padding
          itemExtent: 200,
          itemBuilder: (context, index) {
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 5),
              child: Container(
                margin: const EdgeInsets.symmetric(horizontal: 10),
                color: Colors.blue,
              ),
            );
          },
          itemCount: 20,
        ),
      ),
    );
  }
}

由于比较简单,直接来一张效果图

image.png

下面就新起一个页面介绍 NestedScrollView的效果

NestedScrollView + AppBar + TabBar

话不多说先上一下效果图,避免不易理解

image.png

下面使用 NestedScrollViewheader加入了 SliverAppBar带tabbar的SliverPersistentHeaderbody加入了容器组件 TabBarView

下面充分利用多个SliverAppBar之间相互不影响独立运行的特性,先放置一个SliverAppBar,在放置一个最小和最大高度一样的 SliverPersistentHeader,利用其 pinned 效果,即可实现联动的吸顶效果

PS:可以尝试使用多个 SliverAppBar,享受渐进式展开与合并的效果

class _NestPersistentHeaderViewState extends State<NestPersistentHeaderTabbarView1> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    _controller = TabController(length: tabNames.length, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return <Widget>[
            //系统默认的SliverAppBar
            SliverAppBar(
              expandedHeight: 300.0,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text("六翼天使",
                  style: TextStyle(color: Colors.white, fontSize: 16),),
                background: Image.asset(
                  "images/six_wings_angle_gril.png",
                  fit: BoxFit.cover,
                ),
              ),
            ),
            
            //默认 persistentheader,使用最小间距即可
            SliverPersistentHeader(
              pinned: true,
              delegate: DefaultStickyTabbarDelegate(
                child: TabBar(
                  controller: _controller,
                  labelColor: Colors.black,
                  tabs: tabNames.map((e) => Tab(text: e,)).toList(),
                ),
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _controller,
          children: tabNames.map<Widget>((e) {
            return ListView.builder(
              padding: const EdgeInsets.only(top: 10), //设置padding,避免默认的Padding
              itemExtent: 200,
              itemBuilder: (context, index) {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 5),
                  child: Container(
                    margin: const EdgeInsets.symmetric(horizontal: 10),
                    color: Colors.blue,
                  ),
                );
              },
              itemCount: 20,
            );
          }).toList(),
        ),
      ),
    );
  }
}

DefaultStickyTabbarDelegate也是继承自SliverPersistentHeaderDelegate,就单纯放置了一个Tabbar以及背景而已

//主要是利用 SliverPersistentHeaderDelegate 默认顶部效果
class DefaultStickyTabbarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar child;

  DefaultStickyTabbarDelegate({
    required this.child,
  });

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.white,
      child: child,
    );
  }

  //两个高度一致即可,设置成tabbar的默认高度
  @override
  double get maxExtent => child.preferredSize.height;

  @override
  double get minExtent => child.preferredSize.height;

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

NestedScrollView + 渐变AppBar + 渐变TabBar

看了上面的的 Tabbar 和 Tabbar,发现效果不是那么好,我就想要一个跟淘宝商品详情用的顶部渐变效果怎么办 下面直接搞一个NestedScrollView + 渐变AppBar + 渐变TabBar的仿淘宝详情效果(具体内容可以调整)

也是先上一下对比的效果图,方便理解

image.png

这次因为渐变是联动的,因此 AppbarTabBar 就一起定制到一起,使用 SliverPersistentHeader 一次搞定

这个吸顶效果就是在自定义 AppBar的基础上在下方额外添加了一个渐变的 Tabbar

//自定义顶部效果(这里类似购物详情),需要继承自SliverPersistentHeaderDelegate,这样定制起来更简洁
class StickyTabbarDelegate extends SliverPersistentHeaderDelegate {
  final Widget background;
  final double padding;
  final double collapsedHeight; //收缩高度,appbar和tabbar平分高度
  final double expandHeight;
  final Widget? titleView;
  final TabController? controller;

  StickyTabbarDelegate({
    required this.background,
    required this.collapsedHeight,
    required this.expandHeight,
    this.padding = 0,
    this.controller,
    Widget? titleView,
    String? title,
  })  : assert(titleView == null || title == null, '不能同时传递两个',),
        titleView = titleView ?? Text(title ?? '', style: const TextStyle(color: Colors.black, fontSize: 16,),);

  @override
  double get minExtent => collapsedHeight + padding;

  @override
  double get maxExtent => expandHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox(
      height: maxExtent,
      //需要用到绝对布局,所以使用 Stack
      child: Stack(
        fit: StackFit.expand, //内部铺满
        children: [
          //设置背景
          background,
          //设置 toolbar和tabbar,以及statusbar,避免顶部空出
          Positioned(
            left: 0,
            top: 0,
            right: 0,
            child: Container(
              color: getBkgColor(shrinkOffset),
              //用来设置顶部间距
              child: SafeArea(
                //取消底部间距
                bottom: false,
                //设置toolbar和tabbar
                child: Column(
                  children: [
                    SizedBox(
                      height: collapsedHeight / 2,
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          //这button默认的效果不满意,可以自己使用手势自定义button效果,这里只是写着方便而已
                          MaterialButton(
                            minWidth: 44,
                            padding: const EdgeInsets.symmetric(horizontal: 5),
                            onPressed: () {
                              Navigator.pop(context);
                            },
                            child: Container(
                              width: collapsedHeight / 2.5,
                              height: collapsedHeight / 2.5,
                              decoration: BoxDecoration(
                                color: getBtnBkgColor(shrinkOffset),
                                borderRadius: BorderRadius.circular(collapsedHeight / 4),
                              ),
                              child: Icon(
                                Icons.arrow_back_ios_new,
                                color: getTextColor(shrinkOffset),
                                size: collapsedHeight / 4,
                              ),
                            ),
                          ),
                          Opacity(
                            opacity: getOpacity(shrinkOffset),
                            child: titleView!,
                          ),
                          MaterialButton(
                            minWidth: 44,
                            padding: const EdgeInsets.symmetric(horizontal: 5),
                            onPressed: () {
                              print("点击了分享");
                            },
                            child: Container(
                              width: collapsedHeight / 2.5,
                              height: collapsedHeight / 2.5,
                              decoration: BoxDecoration(
                                color: getBtnBkgColor(shrinkOffset),
                                borderRadius: BorderRadius.circular(collapsedHeight / 4),
                              ),
                              child: Icon(
                                Icons.share,
                                color: getTextColor(shrinkOffset),
                                size: collapsedHeight / 4,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                    Opacity(
                      opacity: getOpacity(shrinkOffset),
                      child: SizedBox(
                        height: collapsedHeight / 2,
                        child: TabBar(
                          labelColor: Colors.black,
                          controller: controller,
                          tabs: tabNames.map((e) => Tab(text: e,)).toList(),
                        ),
                      ),
                    ),
                  ],
                )
              ),
            ),
          ),
        ],
      ),
    );
  }

  //透明度渐变,应用于tabbar和 toolbar中间内容
  double getOpacity(double shrinkOffset) {
    return (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1);
  }

  //背景颜色透明度渐变,应用于整个顶部header背景颜色
  Color getBkgColor(double shrinkOffset) {
    final double opacity = (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1);
    return Color.fromRGBO(0xff, 0xff, 0xff, opacity);
  }

  //设置按钮的背景色渐变,由黑色逐渐透明
  Color getBtnBkgColor(double shrinkOffset) {
    final double opacity = (shrinkOffset / (maxExtent - minExtent) * 2).clamp(0, 1);
    return Color.fromRGBO(0x33, 0x33, 0x33, 1 - opacity);
  }

  //设置文本(即返回和分享等)组件的颜色和透明度渐变
  Color getTextColor(double shrinkOffset) {
    final double opacity = (shrinkOffset / (maxExtent - minExtent)).clamp(0, 1);
    if (opacity <= 0.25) {
      return Color.fromRGBO(0xff, 0xff, 0xff, 1 - opacity);
    }else {
      return Color.fromRGBO(0, 0, 0, opacity);
    }
  }

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    //是否支持重新构建,如果为false,即使外部更新属性,组件也不会被重新构建,根据需要设置即可
    return true;
  }
}

使用还是一如既往,不过要额外传入一个 TabControllertabNames,另外body中的滚动视图会有一个 padding,需要主动设置 padding 可避免此问题

class _NestPersistentHeaderViewState extends State<NestPersistentHeaderTabbarView2> with SingleTickerProviderStateMixin {
  late TabController _controller;
  final List<String> tabs = ["宝贝", "评价", "详情", "推荐"];

  @override
  void initState() {
    _controller = TabController(length: tabs.length, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return <Widget>[
            SliverPersistentHeader(
              pinned: true, //
              delegate: StickyTabbarDelegate(
                background: Image.asset("images/six_wings_angle_gril.png", fit: BoxFit.cover,),
                padding: MediaQuery.of(context).padding.top,
                collapsedHeight: 88,
                expandHeight: 300, //如果不想内部缩放,高度和background一样即可
                controller: _controller,
                tabNames: tabs,
                title: "六翼天使",
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _controller,
          physics: const NeverScrollableScrollPhysics(),
          children: tabs.map<Widget>((e) {
            return ListView.builder(
              padding: const EdgeInsets.only(top: 10), //设置padding,避免默认的Padding
              itemExtent: 200,
              itemBuilder: (context, index) {
                if (index == 0) {
                  return GridView(
                    scrollDirection: Axis.horizontal,
                    padding: const EdgeInsets.symmetric(horizontal: 10),
                    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2,
                      mainAxisSpacing: 10,
                      crossAxisSpacing: 10,
                    ),
                    children: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map<Widget>((e) {
                      return Container(
                        color: Colors.greenAccent,
                      );
                    }).toList(),
                  );
                }
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 5),
                  child: Container(
                    margin: const EdgeInsets.symmetric(horizontal: 10),
                    color: Colors.blue,
                  ),
                );
              },
              itemCount: 20,
            );
          }).toList(),
        ),
      ),
    );
  }
}

看上面代码,可以看到,还额外加入了一个小细节,就是在 ListView.builder 中加入了横向滚动的 GridView,如果没有联动的话,那么直接嵌入是没问题的,还可以避免headerbuilder之间强行下拉后的拖拽后产生间距问题

ps:不是所有的效果都要使用 NestedScrollViewCustomScrollView根据自己的选择合理应对才是关键

最后

快来试一下吧,可能会发现更多好玩的效果,开发起来也有了更多的选择