Dynamic Sliver FloatingActionButton

Dynamic Sliver FloatingActionButton

In this post I will walk through a process of creating FloatingActionButton that is pinned to the edge of FlexibleSpaceBar (shown below). Let’s get to it!

TLDR: I exported pub package, so you can achieve similar FAB behavior. Feel free to use and expand it. 🙂

The problem

Main problem with implementing pinned FloatingActionButton like this, is that there is no place inside the CustomScrollView or SliverList to put a Widget that would be on top of SliverAppBar and Slivers below it. In order to do it, we have to put our FloatingActionButton outside CustomScrollView and place it inside a Stack. Here is our starting point:

class MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Stack(
        children: <Widget>[
          new CustomScrollView(
            slivers: [
              new SliverAppBar(
                expandedHeight: 256.0,
                pinned: true,
                flexibleSpace: new FlexibleSpaceBar(
                  title: new Text("SliverFab Example"),
                  background: new Image.asset(
                    "img.jpg",
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              new SliverList(
                delegate: new SliverChildListDelegate(
                  new List.generate(
                    20,
                    (int index) => new ListTile(title: new Text("Item $index")),
                  ),
                ),
              ),
            ],
          ),
          new Positioned(
            top: 256.0,
            right: 16.0,
            child: new FloatingActionButton(
              onPressed: () {},
              child: new Icon(Icons.add),
            ),
          ),
        ],
      ),
    );
  }
}

It looks like this:

Pinning FloatingActionButton

As you can see, we have beautiful FloatingActionButton pinned to edge of the AppBar but only if user will not scroll the content. Not the best use case for us. In order to pin FloatingActionButton to edge of AppBar, we need to use ScrollController to keep track of scrolling offset and change position of fab. First let’s add ScrollController that will update our Widget’s state:

class MyHomePageState extends State<MyHomePage> {
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = new ScrollController();
    _scrollController.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
  ...
}

Then let’s add it to the CustomScrollView:

new CustomScrollView(
  controller: _scrollController,
  slivers: [
    ...
  ]
)

And then let’s change FloatingActionButton position depending on scrolling offset. To do that I extracted creating FloatingActionButton to new method. A thing that is worth mentioning is that on the first build _scrollController is not connected to scrollView yet, so we can’t directly ask it for offset.

Widget _buildFab() {
  double top = 256.0 - 4.0; //default top margin, -4 for exact alignment
  if (_scrollController.hasClients) {
    top -= _scrollController.offset;
  }
  return new Positioned(
    top: top,
    right: 16.0,
    child: new FloatingActionButton(
      onPressed: () => {},
      child: new Icon(Icons.add),
    ),
  );
}

Current result should look like this:

Hiding FloatingActionButton

As you can see, our FloatingActionButton is scrolling way out of screen. Let’s hide it when it comes close to the top. To do that we could use Opacity widget, however visual aspect of that solution is not great since Fab would be just vanishing. In my opinion better solution would be to scale FloatingActionButton down, so let’s try to do that:

Widget _buildFab() {
  //starting fab position
  final double defaultTopMargin = 256.0 - 4.0;
  //pixels from top where scaling should start
  final double scaleStart = 96.0;
  //pixels from top where scaling should end
  final double scaleEnd = scaleStart / 2;

  double top = defaultTopMargin;
  double scale = 1.0;
  if (_scrollController.hasClients) {
    double offset = _scrollController.offset;
    top -= offset;
    if (offset < defaultTopMargin - scaleStart) {
      //offset small => don't scale down
      scale = 1.0;
    } else if (offset < defaultTopMargin - scaleEnd) {
      //offset between scaleStart and scaleEnd => scale down
      scale = (defaultTopMargin - scaleEnd - offset) / scaleEnd;
    } else {
      //offset passed scaleEnd => hide fab
      scale = 0.0;
    }
  }

  return new Positioned(
    top: top,
    right: 16.0,
    child: new Transform(
      transform: new Matrix4.identity()..scale(scale),
      alignment: Alignment.center,
      child: new FloatingActionButton(
        onPressed: () => {},
        child: new Icon(Icons.add),
      ),
    ),
  );
}

ScaleStart and ScaleEnd values can be changed depending on what behavior you expect. I find this values to satisfy my aesthetic needs 😀

And that’s it! Now we have nice FloatingActionButton placed on the edge of AppBar which looks like this:

Since this kind of view might be needed by other people, I exported it as a pub package. I designed it only for my use case so if you think, there are any bugs or missing features I would highly appreciate any pull requests 🙂

You can find source code created in this post here and current repo with the code here.

If you have any questions, feel free to leave a comment! 🙂
Cheers 🙂

18 thoughts on “Dynamic Sliver FloatingActionButton

  1. hey, I need to add only one item and having the same behavior but when I have a number of items that are not scrolling I can’t scroll the image as you did in above example any solution for that?

  2. Hi, I’m using sliver_fab & it works wonder.
    But may I know what if I want to create multiple fab in sliver by using sliver_fab, how can i achieve that ?

      1. Hi man,
        I don’t think I got what you mean exactly 🙁
        But in order to put multiple fabs, the easiest way would be to just copy sliver_fab and add extra fab directly there, by simple copy-pasting of existing one. At least that’s what I would do 🙂

        1. Hi thank you for your feedback, what I mean is, on is it possible to have 2 sliver_fab on one sliver? how I can do that?

          I’ve tried to do this bt it’s not working

          Builder(
          builder: (context) => new SliverFab(
          floatingWidget: FloatingActionButton(
          onPressed: () => Scaffold.of(context).showSnackBar(SnackBar(content: Text(“You clicked FAB!”))),
          child: Icon(Icons.add),
          ),
          floatingPosition: FloatingPosition(right: 16),
          floatingWidget: FloatingActionButton(
          onPressed: () => Scaffold.of(context).showSnackBar(SnackBar(content: Text(“You clicked FAB!”))),
          child: Icon(Icons.add),
          ),
          floatingPosition: FloatingPosition(right: 16),
          expandedHeight: 140.0,
          expandedHeight: 140.0,

          1. You would need to create extra fields and copy everything that’s related to it IN SliverFab code, like: sliverPostition1, sliverPosition2, sliverPosition3…, or if you prefer you can do it as a list, but I guess you will need to deal with the details by yourself 😉

  3. Hi. Nice article. I am using FutureBuilder Widget in order to fill a SliverList, when I scroll CustomScrollView FutureBuilder is reloaded, then it occurs an exception. How can I solve this?

Leave a Reply

Your email address will not be published.