Mikhail Mazurovskiy
Mikhail Mazurovskiy

Reputation: 185

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.

Upvotes: 3

Views: 2715

Answers (3)

P.Huang
P.Huang

Reputation: 13

I had a similar problem of having a scroll view inside of the body of a nestedScrollView. This answer is very late but NestedScrollView has a setting called floatHeaderSilvers that might have not been available during the time of posting.

So

NestedScrollView(
floatHeaderSlivers: True,
headerSliverBuilder: etc...,
body: etc...
);

Hopefully this helps anyone else who is searching for this question in the future.

Upvotes: 0

Bohdan
Bohdan

Reputation: 126

I had very similar problem with:

  • NestedScrollView
    • SliverAppBar (pinned)
    • TabBarView
      • CustomScrollView
        • SliverPersistentHeader (pinned)
        • Main content

SliverPersistentHeader was scrolled and then pinned under SliverAppBar. SliverPinnedOverlapInjector together with SliverOverlapAbsorber helped.

Upvotes: 0

Diego Alexandre Souza
Diego Alexandre Souza

Reputation: 61

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,
        ),
      ),
    );

Upvotes: 1

Related Questions