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;
}
}