Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pinned SliverPersistentHeader inside CustomScrollView scrolls behind another pinned SliverPersistentHeader inside NestedScrollView

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.

like image 845
Mikhail Mazurovskiy Avatar asked Mar 24 '26 16:03

Mikhail Mazurovskiy


1 Answers

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,
        ),
      ),
    );
like image 53
Diego Alexandre Souza Avatar answered Mar 26 '26 10:03

Diego Alexandre Souza



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!