How to make floating SliverAppBar to show only collapsed bar?

Viewed 81

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

1 Answers

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
Related