Search code examples
flutterdartflutter-websliverappbar

How to make floating SliverAppBar to show only collapsed bar?


I am using a SliverAppBar for Flutter web with a background image, and I would like the bar to disappear when the user is scrolling down the web and appear again as soon as they scroll up, but only the app bar, without showing the background unless they reach the top. Is this accomplishable in Flutter web?

My SliverAppBar:

class NavBar extends StatelessWidget {
  final Widget _background;

  const NavBar(this._background);

  @override
  SliverAppBar build(BuildContext context) {
    double _width = MediaQuery.of(context).size.width;
    double? _height = MediaQuery.of(context).size.height;

    List<Widget> _actions() {
      List<Widget> _list = [];
      List _titles = Navigation(context).routes.keys.toList();
      List _routes = Navigation(context).routes.values.toList();

      _selectView(String route) {
        Navigator.of(context).pushNamed(route);
      }

      Widget _singleItem(String text, String route) {
        return InkWell(
          onTap: () => _selectView(route),
          borderRadius: BorderRadius.circular(15),
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
            alignment: Alignment.center,
            child: Text(
              text,
              style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                  color: Colors.white),
            ),
          ),
        );
      }

      for (int i = 0; i < Navigation(context).showingLinks; i++) {
        _list.add(_singleItem(_titles[i], _routes[i]));
      }

      return _list;
    } // navBarItems

    return SliverAppBar(
      backgroundColor: Theme.of(context).primaryColor,
      expandedHeight: _height,
      pinned: true,
      elevation: 0,
      //TODO make actions appear only when SliverAppBar collapses
      actions: _width > 800 ? _actions() : [],
      flexibleSpace: FlexibleSpaceBar(
        background: _background,
      ),
    );
  }
}

And for the general structure that I am using in all of my views here's an example of my HomeView:

class HomeView extends StatelessWidget {
  final double paddingHorizontal = 60;
  final double paddingVertical = 60;
  ScrollController _scrollController = ScrollController();
  final _key = GlobalKey();

  HomeView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final double width = MediaQuery.of(context).size.width;

     Widget navBarBackground() {
         return Stack(...)
     }

     return Scaffold(
      backgroundColor: Colors.white,
      endDrawer: EndDrawer(),
      body: CustomScrollView(
        controller: _scrollController,
        slivers: [
          NavBar(navBarBackground()),
          SliverList(
            delegate: SliverChildListDelegate(
              [
                highlights(),
                androidIosDesktop(),
                multiplatform(),
                catchPhrase(),
                contact(),
                const Footer(),
              ],
            ),
          )
        ],
      ),
    );
  }
} //HomeView

This is what is shows:

background image app bar

And I would like it to show only this:

app bar without background image


Solution

  • Yes, it is. But, I guess not directly.

    You can use a ScrollController to achieve this. Attach a ScrollController to the CustomScrollView, then observe the offset of the controller where the position is. And based on it, you can achieve the desired output.

    I have written a simple demo code which is the exact thing that you need

    Try out the dartpad => here

    What I did? (look after trying out dartpad)

    1. Add ScrollController to the CustomScrollView
    2. add a const variable moreHeight that needs to expand
    3. add changeable expandedHeight that will be set while listening to the scroll
    4. add a listener to the controller which changes variable expandedHeight depending on the scroll offset.
    5. using this expandedHeight we will change the values of in the SliverAppBar

    Edit:

    Below code is after separating app bar widget which is a stateless widget, and pass the parameters from the view/page that contains scrollview and can use scroll controller (no change in behaviour)

    Note: the following code can be used for the full height expanded app bar

    import 'package:flutter/material.dart';
    
    const 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: const Scaffold(
            body: Center(
              child: MyStatefulWidget(),
            ),
          ),
        );
      }
    }
    
    class MyStatefulWidget extends StatefulWidget {
      const MyStatefulWidget({Key? key}) : super(key: key);
    
      @override
      State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
    }
    
    class _MyStatefulWidgetState extends State<MyStatefulWidget> {
      late ScrollController _scrollController;
      // variable height passed to SliverAppBar expanded height
      double? _expandedHeight;
    
      @override
      initState() {
        super.initState();
        // initialize and add scroll listener
        _scrollController = ScrollController();
        _scrollController.addListener(_scrollListen);
        // initially expanded height is full
        WidgetsBinding.instance.addPostFrameCallback((_) {
          setState(() {
            _expandedHeight = MediaQuery.of(context).size.height;
          });
        });
      }
    
      @override
      dispose() {
        // dispose the scroll listener and controller
        _scrollController.removeListener(_scrollListen);
        _scrollController.dispose();
        super.dispose();
      }
    
      _scrollListen() {
        final offset = _scrollController.offset;
        final height = MediaQuery.of(context).size.height;
        if (offset > height) {
          // if offset is more height, disable expanded height
          if (_expandedHeight != null) {
            setState(() {
              _expandedHeight = null;
            });
          }
        } else {
          // if offset is less, keep increasing the height to offset 0
          setState(() {
            _expandedHeight = height - offset;
          });
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: CustomScrollView(
            controller: _scrollController,
            slivers: <Widget>[
              AppBarWidget(
                expandedHeight: _expandedHeight,
              ),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 2000,
                  child: Center(
                    child: Container(
                      color: Colors.blue,
                    ),
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    
    class AppBarWidget extends StatelessWidget {
      const AppBarWidget({super.key, this.expandedHeight});
    
      final double? expandedHeight;
    
      // constant more height that is given to the expandedHeight
      // of the SliverAppBar
    //   static double moreHeight = 200;
    
      @override
      Widget build(BuildContext context) {
        final height = MediaQuery.of(context).size.height;
        return SliverAppBar(
          pinned: false,
          floating: true,
          expandedHeight: expandedHeight,
          actions: [
            TextButton(
              onPressed: () {},
              child: const Text('test'),
            ),
          ],
          flexibleSpace: FlexibleSpaceBar(
            // animate the opacity offset when expanded height is changed
            background: AnimatedOpacity(
              opacity: expandedHeight != null ? expandedHeight! / height : 0,
              duration: const Duration(milliseconds: 300),
              child: const FlutterLogo(),
            ),
          ),
        );
      }
    }
    

    Edit 2: I have made slight changes to your code

    As I see that you require full height expanded for background, I have included in the above code as well as the your code below.

    NavBar

    1. add new field expandedHeight
    2. made all named parameters
    3. add animated opacity to flexible bar depending on expanded height
    class NavBar extends StatelessWidget {
      const NavBar({this.background, this.expandedHeight});
      
       final double? expandedHeight;
      
       final Widget? background;
    
      @override
      SliverAppBar build(BuildContext context) {
        double _width = MediaQuery.of(context).size.width;
        double? _height = MediaQuery.of(context).size.height;
    
        List<Widget> _actions() {
          List<Widget> _list = [];
          List _titles = Navigation(context).routes.keys.toList();
          List _routes = Navigation(context).routes.values.toList();
    
          _selectView(String route) {
            Navigator.of(context).pushNamed(route);
          }
    
          Widget _singleItem(String text, String route) {
            return InkWell(
              onTap: () => _selectView(route),
              borderRadius: BorderRadius.circular(15),
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
                alignment: Alignment.center,
                child: Text(
                  text,
                  style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                      color: Colors.white),
                ),
              ),
            );
          }
    
          for (int i = 0; i < Navigation(context).showingLinks; i++) {
            _list.add(_singleItem(_titles[i], _routes[i]));
          }
    
          return _list;
        } // navBarItems
    
        return SliverAppBar(
          backgroundColor: Theme.of(context).primaryColor,
          expandedHeight: expandedHeight,
          pinned: true,
          elevation: 0,
          //TODO make actions appear only when SliverAppBar collapses
          actions: _width > 800 ? _actions() : [],
          flexibleSpace: FlexibleSpaceBar(
            // animate the opacity offset when expanded height is changed
            background: AnimatedOpacity(
              opacity: expandedHeight != null ? expandedHeight! / _height : 0,
              duration: const Duration(milliseconds: 300),
              child: background,
            ),
          ),
        );
      }
    }
    

    Home View

    1. Made it stateful widget
    2. add scroll controller, listener, expandedHeight
    class HomeView extends StatefulWidget {
      const HomeView({Key? key}) : super(key: key);
    
      @override
      State<HomeView> createState() => _HomeView();
    }
    
    class _HomeView extends State<HomeView> {
      final double paddingHorizontal = 60;
      final double paddingVertical = 60;
      
      late ScrollController _scrollController;
      // variable height passed to SliverAppBar expanded height
      double? _expandedHeight;
      
      final _key = GlobalKey();
      
      @override
      initState() {
        super.initState();
        // initialize and add scroll listener
        _scrollController = ScrollController();
        _scrollController.addListener(_scrollListen);
        // initially expanded height is full
        WidgetsBinding.instance.addPostFrameCallback((_) {
          setState(() {
            _expandedHeight = MediaQuery.of(context).size.height;
          });
        });
      }
      
      @override
      dispose() {
        // dispose the scroll listener and controller
        _scrollController.removeListener(_scrollListen);
        _scrollController.dispose();
        super.dispose();
      }
    
      _scrollListen() {
        final offset = _scrollController.offset;
        final height = MediaQuery.of(context).size.height;
        if (offset > height) {
          // if offset is more height, disable expanded height
          if (_expandedHeight != null) {
            setState(() {
              _expandedHeight = null;
            });
          }
        } else {
          // if offset is less, keep increasing the height to offset 0
          setState(() {
            _expandedHeight = height - offset;
          });
        }
      }
    
      @override
      Widget build(BuildContext context) {
        final double width = MediaQuery.of(context).size.width;
    
         Widget navBarBackground() {
             return Stack(...)
         }
    
         return Scaffold(
          backgroundColor: Colors.white,
          endDrawer: EndDrawer(),
          body: CustomScrollView(
            controller: _scrollController,
            slivers: [
              NavBar(
                background: navBarBackground(),
                expandedHeight: _expandedHeight,
              ),
              SliverList(
                delegate: SliverChildListDelegate(
                  [
                    highlights(),
                    androidIosDesktop(),
                    multiplatform(),
                    catchPhrase(),
                    contact(),
                    const Footer(),
                  ],
                ),
              )
            ],
          ),
        );
      }
    } //HomeView