前言
我们平时用的 ListView、GridView,看着很够用,但碰到比较较为复杂的功能就显得,例如:淘宝首页这种,因此我们引出了 Sliver,都是可以说滚动视图的尽头是 Sliver,包括ListView、GridView你也可以从他们身上看到Sliver的影子
下面就介绍一下较为常用的 Sliver,话不多说,先上一张美图
Sliver简介
前面说了滚动视图的尽头是 Sliver,可以看到其乃万金油,包裹 ListView、GridView在里面都有替代品,下面先来个三问: Sliver 是什么,里面有什么,可以用来干什么
Sliver 也是一个支持滚动嵌套的 Widget,里面处理了普通滚动视图嵌套一些滚动组件后的冲突问题 (不信,先嵌套一个 Tabbar 试试),使用里面特有的滚动视图,可以无缝嵌套做成我们想要的效果,并且还有一些自带的吸顶等效果方便我们使用
同时 Sliver一旦我们使用习惯了,发现其么不是那么神奇了,即使我们还没读源码,就能解决大部分功能了(还有一些特殊的功能可能仍需要更好的解决方案,如果自己无法解决,可以采用第三方来实现)
Sliver默认使用 CustomScrollView作为底部基础滚动视图,子组件必须是 Sliver 系列的组件,否则会报错(NestedScrollView稍微不一样,虽然和CustomScrollView功能类似,但额外做了一些操作,某些场景使用更方便了一些)
常用的Sliver系列组件有:SliverToBoxAdapter、SliverList、SliverFixedExtentList、SliverGrid、SliverGrid.extent、SliverGrid.count、SliverAppBar、SliverPersistentHeader、SliverSafeArea、SliverFillRemaining等,后面会有案例介绍
SliverToBoxAdapter这里提前介绍,由于Sliver里面只能使用 Sliver系列组件,因此在使用其他自定义组件时需要转化,就是通过该组件,使用如下所示
SliverToBoxAdapter(
child: Container(),//放入自定义组件即可
);
SliverSafeArea(和SafeArea一样)、SliverFillRemaining(填充剩余屏幕空间)
ps:为了方便切换展示查看,Sliver 系列的用 TabBar 把他们放到了一页,因此每一个标签下的才是我们的效果图
SliverList-ListView
先看一小效果图,下面是通过几个SliverList与一个Text文本组成的长列表
先生成一个可以在 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组成的列表,SliverToBoxAdapter 的 title就不多说了
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一页显示更多内容,手机保持原样,这个布局是还是可以的)
更加详细案例,可以参考前面的 GridView 的 SliverGridDelegateWithMaxCrossAxisExtent 代理介绍
//单个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 高度样式)
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协议,可以自定义出更适合自己应用的效果
先看一下自定义的吸顶,效果图(也只比较常见的)
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可以使用日常组件了
下面就是用默认的 SliverAppBar 和 Builder 试试效果
//需要注意的是 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,
),
),
);
}
}
由于比较简单,直接来一张效果图
下面就新起一个页面介绍 NestedScrollView的效果
NestedScrollView + AppBar + TabBar
话不多说先上一下效果图,避免不易理解
下面使用 NestedScrollView,header加入了 SliverAppBar、带tabbar的SliverPersistentHeader,body加入了容器组件 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的仿淘宝详情效果(具体内容可以调整)
也是先上一下对比的效果图,方便理解
这次因为渐变是联动的,因此 Appbar 和 TabBar 就一起定制到一起,使用 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;
}
}
使用还是一如既往,不过要额外传入一个 TabController 和 tabNames,另外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,如果没有联动的话,那么直接嵌入是没问题的,还可以避免header与builder之间强行下拉后的拖拽后产生间距问题
ps:不是所有的效果都要使用 NestedScrollView、CustomScrollView根据自己的选择合理应对才是关键
最后
快来试一下吧,可能会发现更多好玩的效果,开发起来也有了更多的选择