Search code examples
flutterdartflutter-sliversliverappbarcustomscrollview

CustomScrollView: Body scrolls under SliverAppBar


Flutter DartPad

I have multiple SliverAppBar's within a CustomScrollView, the body of the screen is within the SliverFillRemaining.


The Top SliverAppBar is pinned
The Middle SliverAppBar is an image and will collapse as the user scrolls
Bottom SliverAppBar is a TabBar that is pinned and will stay under the First SliverAppBar after the Image has fully collapsed

The current experience is that when you scroll initially, the body scrolls under the lowest SliverAppBar. I have already tried to use SliverOverlapAbsorber/Injector, but that just adds a space to the top of the body so that the spaces get overlapped rather than the body, but this is not what I want.


I want the body and the SliverAppBars to scroll together until the Middle SliverAppBar has collapsed completely, then I want the body to scroll.

I have been working on this for hours, How do you stop the body from being overlapped on scroll?


Solution

  • To achieve this kind of scrolling behaviour it's easier to use NestedScrollView and note that the main appbar isn't in the slivers anymore

    enter image description here

    import 'package:flutter/material.dart';
    
    final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: MyWidget(),
            ),
          ),
        );
      }
    }
    
    class MyWidget extends StatefulWidget {
      @override
      MyWidgetState createState() => MyWidgetState();
    }
    
    class MyWidgetState extends State<MyWidget>
        with SingleTickerProviderStateMixin {
      TabController _tabController;
    
      @override
      void initState() {
        super.initState();
        _tabController = TabController(vsync: this, length: 2);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('title'),
            elevation: 0,
            leading: IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () {},
            ),
          ),
          body: NestedScrollView(
            floatHeaderSlivers: true,
            physics: const BouncingScrollPhysics(),
            body: TabBarView(
              controller: _tabController,
              physics: const NeverScrollableScrollPhysics(),
              children: [
                SingleChildScrollView(
                  physics: const NeverScrollableScrollPhysics(),
                  child: Column(
                    children: List.generate(
                      1000,
                      (index) => Text('Tab One: $index'),
                    ),
                  ),
                ),
                SingleChildScrollView(
                  physics: const NeverScrollableScrollPhysics(),
                  child: Column(
                      children: List.generate(
                    1000,
                    (index) => Text('Tab Two: $index'),
                  )),
                )
              ],
            ),
            headerSliverBuilder: (context, innerBoxIsScrolled) {
              return <Widget>[
                SliverAppBar(
                  pinned: true,
                  floating: false,
                  elevation: 0,
                  toolbarHeight: 0,
                  collapsedHeight: null,
                  automaticallyImplyLeading: false,
                  expandedHeight: MediaQuery.of(context).size.height * .4,
                  flexibleSpace: const FlexibleSpaceBar(
                      collapseMode: CollapseMode.parallax,
                      background: Placeholder()),
                  titleSpacing: 0,
                  primary: false,
                ),
                SliverAppBar(
                  pinned: true,
                  forceElevated: true,
                  primary: false,
                  automaticallyImplyLeading: false,
                  expandedHeight: 50,
                  collapsedHeight: null,
                  toolbarHeight: 50,
                  titleSpacing: 0,
                  title: Align(
                    alignment: Alignment.topCenter,
                    child: TabBar(
                        controller: _tabController,
                        isScrollable: true,
                        tabs: [
                          const Text('Tab One'),
                          const Text('Tab Two'),
                        ]),
                  ),
                ),
              ];
            },
          ),
        );
      }
    }