39

I am using a SliverAppBar in Flutter, with a background widget.

The thing is When it's expanded, the title and icons (leading and actions) should be white in order to be seen correctly, and when it's collapsed, they should be changed to black.

Any ideas on how I can get a bool out of it? Or other ways of resolving this problem.

Thank you.

class SliverExample extends StatefulWidget {
  final Widget backgroundWidget;
  final Widget bodyWidgets;

  SliverExample({
    this.backgroundWidget,
    this.body,
  });
  @override
  _SliverExampleState createState() => _SliverExampleState();
}

class _SliverExampleState extends State<SliverExample> {

  // I need something like this
  // To determine if SliverAppBar is expanded or not.
  bool isAppBarExpanded = false;

  @override
  Widget build(BuildContext context) {

    // To change the item's color accordingly
    // To be used in multiple places in code
    Color itemColor = isAppBarExpanded ? Colors.white : Colors.black;

    // In my case PrimaryColor is white,
    // and the background widget is dark

    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            leading: BackButton(
              color: itemColor, // Here
            ),
            actions: <Widget>[
              IconButton(
                icon: Icon(
                  Icons.shopping_cart,
                  color: itemColor, // Here
                ),
                onPressed: () {},
              ),
            ],
            expandedHeight: 200.0,
            flexibleSpace: FlexibleSpaceBar(
              centerTitle: true,
              title: Text(
                'title',
                style: TextStyle(
                  fontSize: 18.0,
                  color: itemColor, // Here
                ),
              ),
              // Not affecting the question.              
              background: widget.backgroundWidget,
            ),
          ),
          // Not affecting the question.
          SliverToBoxAdapter(child: widget.body),
        ],
      ),
    );
  }
}
Sina Seirafi
  • 2,073
  • 3
  • 15
  • 16

5 Answers5

98

You can use LayoutBuilder to get sliver AppBar biggest height. When biggest.height = MediaQuery.of(context).padding.top + kToolbarHeight, it actually means sliver appbar is collapsed.

Here is a full example, in this example MediaQuery.of(context).padding.top + kToolbarHeight = 80.0:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      home: MyApp(),
    ));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var top = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[
          SliverAppBar(
              expandedHeight: 200.0,
              floating: false,
              pinned: true,
              flexibleSpace: LayoutBuilder(
                  builder: (BuildContext context, BoxConstraints constraints) {
                // print('constraints=' + constraints.toString());
                top = constraints.biggest.height;
                return FlexibleSpaceBar(
                    centerTitle: true,
                    title: AnimatedOpacity(
                        duration: Duration(milliseconds: 300),
                        //opacity: top == MediaQuery.of(context).padding.top + kToolbarHeight ? 1.0 : 0.0,
                        opacity: 1.0,
                        child: Text(
                          top.toString(),
                          style: TextStyle(fontSize: 12.0),
                        )),
                    background: Image.network(
                      "https://images.unsplash.com/photo-1542601098-3adb3baeb1ec?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=5bb9a9747954cdd6eabe54e3688a407e&auto=format&fit=crop&w=500&q=60",
                      fit: BoxFit.cover,
                    ));
              })),
        ];
      },body: ListView.builder(
        itemCount: 100,
        itemBuilder: (context,index){
          return Text("List Item: " + index.toString());
        },
      ),
    ));
  }
}

Final result:

enter image description here

Jack Sun
  • 2,012
  • 1
  • 16
  • 20
  • Hi! Question. Why does it fully expand to 224 when you've set `expandedHeight` to 200? And is there any way to get that 224 max number without it changing when you scroll? Right now I'm using it to map the appbar height from 0.0 to 1.0 for a smooth opacity transition throughout (although there's probably an easier way to do it than I am trying) – Nolence Apr 29 '19 at 18:44
  • 24
    Do not hardcode the 80 as it is different on different devices! It's actually the status bar height + the app bar height, which you can get using the following: `MediaQuery.of(context).padding.top + kToolbarHeight` – Ovidiu Jan 16 '20 at 12:42
  • This is absolutely brilliant, thanks Jack! – Hank Dec 10 '20 at 17:11
  • Does this work when you open and close the keyboard or only when scrolling? The sliverappbar collapses when you open the keyboard (assuming enough content in the screen), and expands when you close the keyboard. I am trying to figure out how to know when the keyboard opens and closes. Build isn't triggered when keyboard opens and closes. – Barry Jun 05 '21 at 21:12
  • @Ovidiu use `scrollController.position.maxScrollExtent` and `scrollController.position.minScrollExtent` instead – Omar Fayad May 31 '23 at 14:43
16

You need to use ScrollController to achieve the desired effect

try this 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: SliverExample(
        bodyWidgets: Text(
            'Hello Body gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg'),
        backgroundWidget: Text('Hello Background'),
      ),
    );
  }
}

class SliverExample extends StatefulWidget {
  final Widget backgroundWidget;
  final Widget bodyWidgets;

  SliverExample({
    this.backgroundWidget,
    this.bodyWidgets,
  });

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

class _SliverExampleState extends State<SliverExample> {

  ScrollController _scrollController;
  Color _theme ;

  @override
  void initState() {
    super.initState();
    _theme = Colors.black;

    _scrollController = ScrollController()
      ..addListener(
        () => _isAppBarExpanded ?
            _theme != Colors.white ?
        setState(
          () {
            _theme = Colors.white;
            print(
                'setState is called');
          },
        ):{}
            : _theme != Colors.black ?
        setState((){
          print(
              'setState is called');
          _theme = Colors.black;
        }):{},

      );
  }

  bool get _isAppBarExpanded {
    return _scrollController.hasClients &&
        _scrollController.offset > (200 - kToolbarHeight);
  }

  @override
  Widget build(BuildContext context) {
    // To change the item's color accordingly
    // To be used in multiple places in code
    //Color itemColor = isAppBarExpanded ? Colors.white : Colors.black;

    // In my case PrimaryColor is white,
    // and the background widget is dark

    return Scaffold(
      body: CustomScrollView(
        controller: _scrollController,
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            leading: BackButton(
              color: _theme, // Here
            ),
            actions: <Widget>[
              IconButton(
                icon: Icon(
                  Icons.shopping_cart,
                  color: _theme, // Here
                ),
                onPressed: () {},
              ),
            ],
            expandedHeight: 200.0,
            flexibleSpace: FlexibleSpaceBar(
              centerTitle: true,
              title: Text(
                'title',
                style: TextStyle(
                  fontSize: 18.0,
                  color: _theme, // Here
                ),
              ),
              // Not affecting the question.
              background: widget.backgroundWidget,
            ),
          ),
          // Not affecting the question.
          SliverToBoxAdapter(child: widget.bodyWidgets),
        ],
      ),
    );
  }
}

if you are not familiar with ? : notation you can use the following

_scrollController = ScrollController()
      ..addListener(
          (){
            if(_isAppBarExpanded){
              if(_theme != Colors.white){
                setState(() {
                  _theme = Colors.white;
                  print('setState is called with white');
                });
              }
            }else{
              if(_theme != Colors.black){
                setState(() {
                  _theme = Colors.black;
                  print('setState is called with black');
                });
              }
            }
          }
Saed Nabil
  • 6,705
  • 1
  • 14
  • 36
  • This solution make my ListView refresh everytime i scrolled, – Zeffry Reynando Oct 13 '19 at 06:58
  • @ZeffryReynando thanks for highlighting this , you can actually use if else to check if the rebuild is needed and call setState accordingly , I edited my answer to reflect that but used the ? : notation as I used to it , hope that it will be clear to you – Saed Nabil Oct 13 '19 at 13:28
  • I'm little confuse you make `if else` with notation `? :` Especially `_isAppBarExpanded ? _theme != Colors.white ?` can you explain using traditional `if else` ? – Zeffry Reynando Oct 14 '19 at 03:47
  • @ZeffryReynando check the added portion for using traditional if else statement – Saed Nabil Oct 14 '19 at 16:38
12

Use NestedScrollView that have innerBoxIsScrolled boolean flag that will be a decent solution for your problem something like this below

NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          print("INNEER SCROLLED VI=======>$innerBoxIsScrolled");
          return <Widget>[
            SliverAppBar(), 
            ]},
body:Center(child:Text("Test IT")),

);
Uzair Leo
  • 121
  • 1
  • 3
  • this should be the accepted answer. the other answer just listen the variable height inside `flexibleSpace`, but this one really to the point for detecting whether the appbar is expanded or collapsed and can be accessed outside `flexibleSpace` without really writing any additional codes. – Taufik Nur Rahmanda Dec 08 '21 at 22:51
  • you guys welcome actually NestedScrollView is very handy go read it docs you can use it many more events and avoid boilerplate code – Uzair Leo Dec 09 '21 at 23:15
6

You can use SliverLayoutBuilder to get the current SliverConstraints and read its value, to easily detect how much scrolling has occurred. This is very similar to LayoutBuilder except it's operating in the sliver-world.

If constraints.scrollOffset > 0, that means the user has scrolled at least a little bit. Using this method, if you want to do some animation/transition when scrolling, it's easy too - just get the current scrollOffset and generate your animation frame based on that.

For example, this SliverAppBar changes color when user scrolls:

  SliverLayoutBuilder(
    builder: (BuildContext context, constraints) {
      final scrolled = constraints.scrollOffset > 0;
      return SliverAppBar(
        title: Text('Sliver App Bar'),
        backgroundColor: scrolled ? Colors.blue : Colors.red,
        pinned: true,
      );
    },
  )
WSBT
  • 33,033
  • 18
  • 128
  • 133
3

This works for me check this line

title: Text(title,style: TextStyle(color: innerBoxIsScrolled? Colors.black:Colors.white),),

change your title to

title: innerBoxIsScrolled? Text("hello world") : Text("Good Morning",)
class _ProductsState extends State<Products> {
  String image;
  String title;

  _ProductsState(this.image,this.title);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
          return <Widget>[
            SliverOverlapAbsorber(
              handle:
              NestedScrollView.sliverOverlapAbsorberHandleFor(context),
              sliver: SliverAppBar(
                leading: InkWell(
                  onTap: (){

                  },
                  child: Icon(
                    Icons.arrow_back_ios,
                    color: Colors.black,
                  ),
                ),
                backgroundColor: Colors.white,
                pinned: true,
                //floating: true,
                stretch: true,
                expandedHeight: 300.0,
                flexibleSpace: FlexibleSpaceBar(
                  centerTitle: true,
                  title: Text(title,style: TextStyle(color: innerBoxIsScrolled? Colors.black: Colors.white),),
                  background: CachedNetworkImage(imageUrl:image,fit: BoxFit.fitWidth,alignment: Alignment.topCenter,
                    placeholder: (context, url) => const CircularProgressIndicator(color: Colors.black,),
                    errorWidget: (context, url, error) => const Icon(Icons.error),),
                ),
              ),
            ),
          ];
        },
        body: Builder(
            builder:(BuildContext context) {
              return CustomScrollView(
                slivers: [
                  SliverOverlapInjector(
                    // This is the flip side of the SliverOverlapAbsorber above.
                    handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                        context),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 90,
                      color: Colors.black,
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 200,
                      color: Colors.red,
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Container(
                        height: 200,
                        color: Colors.green,
                      ),
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 200,
                      color: Colors.blue,
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 200,
                      color: Colors.red,
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 200,
                      color: Colors.blue,
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 200,
                      color: Colors.red,
                    ),
                  ),
                ],
              );
            }
        ),
      ),
    );
  }
}