Background
I have two SliverPersistentHeaders. One is located inside NestedScrollView and has a TabBar inside of it which should be always visible on scroll, so I made it pinned. Also I have another SliverPersistentHeader which is located inside CustomScrollView in one of the tabs. It should be always visible on scroll when this tab is opened, so I made it pinned also.
Here is what I have visually. The SliverPersistentHeaders I write about are white and blue:
https://ibb.co/JzZPyqp
The Problem
I expect both of the SliverPersistentHeaders to be pinned and not go up on scroll. But it turns out the SliverPersistentHeader inside CustomScrollView scrolls behind the SliverPersistentHeader in NestedScrollView and only then gets pinned like so:
https://s2.gifyu.com/images/untitled446dfde99b45261c.gif
Let's jump to the code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
var _tabController;
@override
void initState() {
_tabController = TabController(
length: 2,
vsync: this,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
physics: BouncingScrollPhysics(),
floatHeaderSlivers: true,
headerSliverBuilder: (context, value) {
return [
// here we have the first SliverPersistentHeader
// with TabBar as content
// inside of NestedScrollView
SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _TabBarAsSliverPersistentHeader(_tabController),
),
];
},
body:
// BlocProvider might be here in the real project...
CustomScrollView(
slivers: [
// here we have the second SliverPersistentHeader
// inside of CustomScrollView
// it has unexpected behaviour as shown in gif above
SliverPersistentHeader(
pinned: true,
floating: false,
delegate: PersistentHeaderDateFilter(),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
height: 100,
margin: EdgeInsets.all(10),
color: Colors.red,
),
childCount: 10,
),
)
],
),
),
),
);
}
}
class _TabBarAsSliverPersistentHeader extends SliverPersistentHeaderDelegate {
final TabController _tabController;
_TabBarAsSliverPersistentHeader(this._tabController);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
// TODO: implement build
return Container(
// color: Theme.of(context).backgroundColor,
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
),
),
),
),
),
Container(
color: Colors.transparent,
child: TabBar(
isScrollable: false,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
width: 2,
color: Colors.blue,
),
),
indicatorSize: TabBarIndicatorSize.tab,
indicatorWeight: 2,
labelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(text: 'by rating'),
Tab(text: 'by date'),
],
controller: _tabController,
),
),
],
),
);
}
@override
double get maxExtent => 46.0;
@override
double get minExtent => 46.0;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
class PersistentHeaderDateFilter extends SliverPersistentHeaderDelegate {
PersistentHeaderDateFilter();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
// TODO: implement build
return Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.blue.shade200,
border: Border(
bottom: BorderSide(
color: Colors.green,
width: 1.0,
),
),
),
alignment: Alignment.center,
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20),
// shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: 10,
// ignore: missing_return
itemBuilder: (context, index) {
return Container(
color: Colors.amber,
margin: EdgeInsets.all(5),
width: 50,
child: Text(index.toString()),
);
},
),
);
}
@override
double get maxExtent => 110.0;
@override
double get minExtent => 110.0;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
What I tried
So far I have experimented with SliverOverlapAbsorber and SliverOverlapInjector which are mentioned in flutter doc of NestedScrollView but without any good results. Moving TabBar to SliverAppBar bottom property also was not successful.
What worked for me:
I've wrapped the whole SliverPersistentHeader (my _buildTabNavigation()) in SliverOverlapAbsorver:
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: _buildTabNavigation(),
),
Doing this, your list will be stuck under the TabBar. Then you can wrap your NestedScrollView body in a Container and add a margin top.
Something like this:
return NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, value) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: _buildTabNavigation(),
),
];
},
body: Container(
margin: EdgeInsets.only(top: 50),
child: TabBarView(
controller: _tabController,
children: _tabs,
),
),
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With