1

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

JAgüero
  • 403
  • 1
  • 4
  • 14

1 Answers1

2

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
immadisairaj
  • 608
  • 4
  • 11
  • I have tried to do this but I found a problem: my navigationBar is in a different file than my web views, since it's a widget that I use in all of them, so I cannot use `_scrollController` on it and I cannot pass it as a parameter because it's a StatefulWidget. – JAgüero May 25 '22 at 08:39
  • for controlling states, `StatefulWidget` works. I think you meant `StatelessWidget`. But, making it `StatefulWidget` might do the job. Or, you don't need to pass the `scrollController`, you can listen to it in the scroll view, then pass the `expandedHeight` parameter to it. `moreHeight` is a constant, so that can be set static inside your appbar widget. – immadisairaj May 25 '22 at 14:53
  • I have edited the answer, and added the code. I think that might be where you are facing the problem. – immadisairaj May 25 '22 at 15:07
  • No, I am sorry for the confusion. I mean that the navigation Bar that you showed previously on the dartpad was stateful, so I did not know how to do it with that (I know provider exist but I don't know how to use it, I don't know if that would be the solution?). I also tried the new solution and cannot use it like that because I have an argument when calling to AppBarWidget. I edited the question and added my whole code. Thank you so much for your help. – JAgüero May 25 '22 at 21:09
  • It is easier to change it to a `StatefulWidget` fro `StatelessWidget` than to use `provider`. You could convert the `HomeView` to `StatefulWidget` which is `MyStatefulWidget` in the above code, and the `AppBarWidget` is the same as your `NavBar` widget. If this doesn't help, I will update the answer to include the changes I suggested into your code. – immadisairaj May 26 '22 at 05:43
  • I have updated the answer, please have a look at it. – immadisairaj May 26 '22 at 06:17
  • 1
    Thank you! It works, the only thing now is that it starts expanding a little bit before reaching the top and that I have various screens, so I would have to repeat the code in all of them. I will try now to extract it to another file so I can reuse it. Thank you again :) – JAgüero Jun 19 '22 at 16:24