10

So there are many examples on the web where you can use a SliverAppBar that hides on scroll, and the TabBar below is still showing. I can't find anything that does it the other way around: When I scroll up I want to hide only the TabBar, keeping the AppBar persistent showing at all times. Does anyone know how to achieve this?

Here is a example with AppBar hiding (This is not what I want, just helps understand better what I want).

UPDATE

This is what I tried so far, and I thought it works, but the problem is I can't get the AppBar in the Positioned field to have the correct height (e.g. iPhone X its height is way bigger and overlaps with the tab bar).

// this sliver app bar is only use to hide/show the tabBar, the AppBar  
// is invisible at all times. The to the user visible AppBar is below
return Scaffold(
  body: Stack(
    children: <Widget>[
      NestedScrollView(
        headerSliverBuilder:
            (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              floating: true,
              snap: true,
              pinned: false,
              bottom: TabBar(
                tabs: [
                  Tab(
                    child: Text(
                      "1",
                      textAlign: TextAlign.center,
                    ),
                  ),
                  Tab(
                    child: Text(
                      "2",
                      textAlign: TextAlign.center,
                    ),
                  ),
                  Tab(
                    child: Text(
                      "3",
                      textAlign: TextAlign.center,
                    ),
                  ),
                ],
                controller: _tabController,
              ),
            ),
          ];
        },
        body: TabBarView(
          children: [
            MyScreen1(),
            MyScreen2(),
            MyScreen3(),
          ],
          controller: _tabController,
          physics: new NeverScrollableScrollPhysics(),
        ),
      ),


      // Here is the AppBar the user actually sees. The SliverAppBar 
      // above will slide the TabBar underneath this one. However,
      // I can´t figure out how to give it the correct height.
      Container(
        child: Positioned(
          top: 0.0,
          left: 0.0,
          right: 0.0,
          child: AppBar(
            iconTheme: IconThemeData(
              color: Colors.red, //change your color here
            ),
            automaticallyImplyLeading: true,
            elevation: 0,
            title: Text("My Title"),
            centerTitle: true,

          ),
        ),
      ),

    ],

  ),
);
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
最白目
  • 3,505
  • 6
  • 59
  • 114

3 Answers3

7

Here is How you can do that, the idea is to use a postframecallback with the help of a GlobalKey to precalculate the appBar height and add an exapandedHeight like below,

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);


  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin  {

  TabController _tabController;
  GlobalKey _appBarKey;
  double _appBarHight;
  @override
  void initState() {
    _appBarKey = GlobalKey();
    _tabController = TabController(length: 3, vsync: this);
    SchedulerBinding.instance.addPostFrameCallback(_calculateAppBarHeight);
    super.initState();
  }
  _calculateAppBarHeight(_){
    final RenderBox renderBoxRed = _appBarKey.currentContext.findRenderObject();
     setState(() {
  _appBarHight = renderBoxRed.size.height;
});
    print("AppbarHieght = $_appBarHight");
  }

  @override
  Widget build(BuildContext context) {
    // this sliver app bar is only use to hide/show the tabBar, the AppBar
    // is invisible at all times. The to the user visible AppBar is below
    return Scaffold(
      body: Stack(
        children: <Widget>[
          NestedScrollView(
            headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
              return <Widget>[
                SliverAppBar(
                  floating: true,
                  expandedHeight: _appBarHight,
                  snap: true,
                  pinned: false,
                  bottom: TabBar(
                    tabs: [
                      Tab(
                        child: Text(
                          "1",
                          textAlign: TextAlign.center,
                        ),
                      ),
                      Tab(
                        child: Text(
                          "2",
                          textAlign: TextAlign.center,
                        ),
                      ),
                      Tab(
                        child: Text(
                          "3",
                          textAlign: TextAlign.center,
                        ),
                      ),
                    ],
                    controller: _tabController,
                  ),
                ),
              ];
            },
            body: TabBarView(
              children: [
                MyScreen1(),
                MyScreen2(),
                MyScreen3(),
              ],
              controller: _tabController,
              physics: new NeverScrollableScrollPhysics(),
            ),
          ),


          // Here is the AppBar the user actually sees. The SliverAppBar
          // above will slide the TabBar underneath this one. However,
          // I can¥t figure out how to give it the correct height.
          Container(
            key: _appBarKey,
            child: Positioned(
              top: 0.0,
              left: 0.0,
              right: 0.0,
              child: AppBar(

                backgroundColor: Colors.red,
                iconTheme: IconThemeData(
                  color: Colors.red, //change your color here
                ),
                automaticallyImplyLeading: true,
                elevation: 0,
                title: Text("My Title"),
                centerTitle: true,

              ),
            ),
          ),

        ],

      ),
    );
  }

}

class MyScreen1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("My Screen 1"),
      ),
    );
  }
}
class MyScreen2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("My Screen 2"),
      ),
    );
  }
}
class MyScreen3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("My Screen 3"),
      ),
    );
  }
}

Edit:

After more investigation I found a solution without keys or MediaQuery "stuff" by using just SafeArea Widget . please check the following Complete 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,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);


  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin  {

  TabController _tabController;

  @override
  void initState() {

    _tabController = TabController(length: 3, vsync: this);

    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    // this sliver app bar is only use to hide/show the tabBar, the AppBar
    // is invisible at all times. The to the user visible AppBar is below
    return Scaffold(
      body: Stack(
        children: <Widget>[
          NestedScrollView(

            headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
              return <Widget>[
                SliverAppBar(
                  primary: true,
                  floating: true,
                  backgroundColor: Colors.blue,//.withOpacity(0.3),
                  snap: true,
                  pinned: false,
                  bottom: TabBar(
                    tabs: [
                      Tab(
                        child: Text(
                          "1",
                          textAlign: TextAlign.center,
                        ),
                      ),
                      Tab(
                        child: Text(
                          "2",
                          textAlign: TextAlign.center,
                        ),
                      ),
                      Tab(
                        child: Text(
                          "3",
                          textAlign: TextAlign.center,
                        ),
                      ),
                    ],
                    controller: _tabController,
                  ),
                ),
              ];
            },
            body: TabBarView(
              children: [
                MyScreen1(),
                MyScreen2(),
                MyScreen3(),
              ],
              controller: _tabController,
              physics: new NeverScrollableScrollPhysics(),
            ),
          ),


          // Here is the AppBar the user actually sees. The SliverAppBar
          // above will slide the TabBar underneath this one. 
          // by using SafeArea it will.
          Positioned(
            top: 0.0,
            left: 0.0,
            right: 0.0,
            child: Container(
              child: SafeArea(
                top: false,
                child: AppBar(
                  backgroundColor: Colors.blue,
//                iconTheme: IconThemeData(
//                  color: Colors.red, //change your color here
//                ),
                  automaticallyImplyLeading: true,
                  elevation: 0,
                  title: Text("My Title",),
                  centerTitle: true,
                ),
              ),
            ),
          ),

        ],

      ),
    );
  }

}

class MyScreen1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      child: Center(
        child: Text("My Screen 1"),
      ),
    );
  }
}
class MyScreen2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("My Screen 2"),
      ),
    );
  }
}
class MyScreen3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("My Screen 3"),
      ),
    );
  }
}
Saed Nabil
  • 6,705
  • 1
  • 14
  • 36
  • Sorry for not responding in a while. I came to the exact same solution in the meantime: SafeArea solves all my problems. Good job! – 最白目 Jun 12 '19 at 13:07
3

Screenshot (Android)

enter image description here

Screenshot (iPhone X)

enter image description here


Your were very close, I have just modified couple of lines. I did it without using GlobalKey and other stuff (postFrameCallback etc). It is very simple and straightforward approach.

All you need to do is replace FlutterLogo with your own widgets which are MyScreen1, MyScreen2 and MyScreen3.


Code

void main() => runApp(MaterialApp(home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          NestedScrollView(
            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
              return <Widget>[
                SliverAppBar(
                  floating: true,
                  snap: true,
                  pinned: true,
                  bottom: PreferredSize(
                    preferredSize: Size(0, kToolbarHeight),
                    child: TabBar(
                      controller: _tabController,
                      tabs: [
                        Tab(child: Text("1")),
                        Tab(child: Text("2")),
                        Tab(child: Text("3")),
                      ],
                    ),
                  ),
                ),
              ];
            },
            body: TabBarView(
              controller: _tabController,
              children: [
                FlutterLogo(size: 300, colors: Colors.blue), // use MyScreen1()
                FlutterLogo(size: 300, colors: Colors.orange), // use MyScreen2()
                FlutterLogo(size: 300, colors: Colors.red), // use MyScreen3()
              ],
              physics: NeverScrollableScrollPhysics(),
            ),
          ),
          Positioned(
            top: 0.0,
            left: 0.0,
            right: 0.0,
            child: MediaQuery.removePadding(
              context: context,
              removeBottom: true,
              child: AppBar(
                iconTheme: IconThemeData(color: Colors.red),
                automaticallyImplyLeading: true,
                elevation: 0,
                title: Text("My Title"),
                centerTitle: true,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • Good job:) but, if we know the hight of the app bar to be 56 then there is no problem from the first place. if you tried my solution you will find that the height is 134 in iPhone X not 56 hence the question ,I think we had a similar discussion once !. – Saed Nabil Jun 08 '19 at 21:21
  • Glad to see you again, well I didn't try your solution, but as far as I can tell, the height in iPhone X won't be `134` instead it would be `100` (`44` for status bar + `56` for navigation bar), but not sure what height you are considering when saying `134`, – CopsOnRoad Jun 09 '19 at 06:56
  • Me too, your solution is valid only on the perfect world :) ,IPhone X has different design dimensions and 56 is much shorter for the appbar than it should , check this question https://stackoverflow.com/questions/46197660/what-is-the-top-bar-height-of-iphone-x – Saed Nabil Jun 09 '19 at 09:27
  • I am not sure by term `pt` there, I come from Android background where we have `dp`, (not sure if this is what `pt` in iOS), but for now, I can check in iPhone XR (and iPhone X shouldn't be different) the status bar is `44` logical pixels high and navigation bar is `56` logical pixels which makes it `100` – CopsOnRoad Jun 09 '19 at 09:36
  • @SaedNabil I ran the same code in iPhone X and seems like your so called "solution for perfect world" worked flawlessly here too with `56`. – CopsOnRoad Jun 09 '19 at 12:14
  • please compare these two images image1 https://imgur.com/CNZjfSG image two https://imgur.com/w2Iey55 you may notice the difference in appbar height , the red appbar is the standard height for iPhone X , and I know that it works but the issue is it is not the standard height as I explained earlier. – Saed Nabil Jun 09 '19 at 12:46
  • Yes, you are right there is a difference, and I didn't test it on iOS when I was writing code, I ran it on Android (whose screenshot I uploaded first), but again you don ' t need to deal with `134`, I have done the same effect with `100` in the updated code and now it is working for iOS too. – CopsOnRoad Jun 09 '19 at 13:10
  • I do not deal with any constants at all niether 134 nor 56 the code dynamically calculate the hight and adapt to any device dimensions ,I checked your code and it seams nothing has changed regarding the height ,please check this image https://imgur.com/ciXHjC7 – Saed Nabil Jun 09 '19 at 14:55
  • Even I didn't play with `56` anymore, I used `kToolbarHeight` (hope you won't point on this now), and the screenshot you sent is the same you get when you use `appBar: AppBar() `, so there isn't anything wrong with that. – CopsOnRoad Jun 09 '19 at 14:57
  • That was very interesting :) ,I think I found the correct solution , please check the Edit section of my answer – Saed Nabil Jun 10 '19 at 01:44
  • I am on holiday for couple of days and can't understand any code part (using phone to comment), if you liked my solution, I will wait for +1 from your end :) – CopsOnRoad Jun 10 '19 at 08:10
0

I think its pretty easy using nested scaffolds. where you dont need to calculate any height. Just put the tabbar inside a SilverAppBar not below the SilverAppBar.

feel free to comment if that doesnt solve your problem.

Example:

return Scaffold(
     appBar: AppBar(), //your appbar that doesnt need to hide
     body: Scaffold(
           appBar: SilverAppBar(

            pinned: false,
            floating: false,

            flexibleSpace: new Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              new TabBar() //your tabbar that need to hide when scrolling
             ])
             )

             body: //your content goes here

             )

);
Madlad 007
  • 197
  • 1
  • 3
  • 13
  • have you tried it yourself? It gives me error `The argument type 'SliverAppBar' can't be assigned to the parameter type 'PreferredSizeWidget?'.` – RDK Aug 06 '21 at 07:49