Flutter - Hiding FloatingActionButton

Viewed 45724

Is there any built in way in Flutter to hide a FloatingActionButton on ListView scrolling down and then showing it on scrolling up?

9 Answers

Without animation:

  • Using Visibility widget:

    floatingActionButton: Visibility(
      visible: false, // Set it to false
      child: FloatingActionButton(...),
    )
    
  • Using Opacity widget:

    floatingActionButton: Opacity(
      opacity: 0, // Set it to 0
      child: FloatingActionButton(...),
    )
    
  • Using ternary operator:

    floatingActionButton: shouldShow ? FloatingActionButton() : null,
    
  • Using if condition:

    floatingActionButton: Column(
      children: <Widget>[
        if (shouldShow) FloatingActionButton(...), // Visible if condition is true
      ],
    )
    

With animation:

enter image description here

This is just one example of using animation, you can create different types of UI using this approach.

bool _showFab = true;
  
@override
Widget build(BuildContext context) {
  const duration = Duration(milliseconds: 300);
  return Scaffold(
    floatingActionButton: AnimatedSlide(
      duration: duration,
      offset: _showFab ? Offset.zero : Offset(0, 2),
      child: AnimatedOpacity(
        duration: duration,
        opacity: _showFab ? 1 : 0,
        child: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {},
        ),
      ),
    ),
    body: NotificationListener<UserScrollNotification>(
      onNotification: (notification) {
        final ScrollDirection direction = notification.direction;
        setState(() {
          if (direction == ScrollDirection.reverse) {
            _showFab = false;
          } else if (direction == ScrollDirection.forward) {
            _showFab = true;
          }
        });
        return true;
      },
      child: ListView.builder(
        itemCount: 100,
        itemBuilder: (_, i) => ListTile(title: Text('$i')),
      ),
    ),
  );
}

Quite an old question, but with the latest flutter there is a nicer (and shorter) solution in my opinion.

The other solutions do work, but if you want a nice animation (comparable to the default Animation in Android), here you go:

A NotificationListener informs you, whenever a user scrolls (up/down). With an AnimationController you can control the animation of the FAB.

Here's a full example:

class WidgetState extends State<Widget> with TickerProviderStateMixin<Widget> {
  AnimationController _hideFabAnimation;

  @override
  initState() {
    super.initState();
    _hideFabAnimation = AnimationController(vsync: this, duration: kThemeAnimationDuration);
  }

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

  bool _handleScrollNotification(ScrollNotification notification) {
    if (notification.depth == 0) {
      if (notification is UserScrollNotification) {
        final UserScrollNotification userScroll = notification;
        switch (userScroll.direction) {
          case ScrollDirection.forward:
            if (userScroll.metrics.maxScrollExtent !=
                userScroll.metrics.minScrollExtent) {
              _hideFabAnimation.forward();
            }
            break;
          case ScrollDirection.reverse:
           if (userScroll.metrics.maxScrollExtent !=
                userScroll.metrics.minScrollExtent) {
              _hideFabAnimation.reverse();
            }
            break;
          case ScrollDirection.idle:
            break;
        }
      }
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollNotification>(
      onNotification: _handleScrollNotification,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Fabulous FAB Animation')
        ),
        body: Container(),
        floatingActionButton: ScaleTransition(
          scale: _hideFabAnimation,
          alignment: Alignment.bottomCenter,
          child: FloatingActionButton(
            elevation: 8,
            onPressed: () {},
            child: Icon(Icons.code),
          ),
        ),
      ),
    );
  }
}

A good way to do it...

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  ScrollController controller;
  bool fabIsVisible = true;

  @override
  void initState() {
    super.initState();
    controller = ScrollController();
    controller.addListener(() {
      setState(() {
        fabIsVisible =
            controller.position.userScrollDirection == ScrollDirection.forward;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        controller: controller,
        children: List.generate(
            100,
            (index) => ListTile(
                  title: Text("Text $index"),
                )),
      ),
      floatingActionButton: AnimatedOpacity(
        child: FloatingActionButton(
          child: Icon(Icons.add),
          tooltip: "Increment",
          onPressed: !fabIsVisible ? null: () {
            print("Pressed");
          },
        ),
        duration: Duration(milliseconds: 100),
        opacity: fabIsVisible ? 1 : 0,
      ),
    );
  }
}

you can use below code to keep default animation

floatingActionButton: _isVisible
        ? FloatingActionButton(...)
        : null,

You can use Visibility widget for handling the Visibility of child widget

sample :

  floatingActionButton:
            Visibility(visible: _visibilityFlag , child: _buildFAB(context)),

Other very good way is AnimatedOpacity

AnimatedOpacity(
          opacity: isEnabled ? 0.0 : 1.0,
          duration: Duration(milliseconds: 1000),
          child: FloatingActionButton(
             onPressed: your_method,
             tooltip: 'Increment',
             child: new Icon(Icons.add),
          ),
        )

For anyone using Rxdart, there is a terse way to do this, and it comes with extra handy tools.

First, convert scroll position to stream, you can reuse this method for later as well.


extension ScrollControllerX on ScrollController {
  Stream<double> positionAsStream() {
    late StreamController<double> controller;

    void addListener() => controller.add(position.pixels);
    void onListen() => this.addListener(addListener);
    void onCancel() {
      removeListener(addListener);
      controller.close();
    }

    controller = StreamController<double>(onListen: onListen, onCancel: onCancel);

    return controller.stream;
  }
}

Use it like this.


      @override
      void initState() {
        super.initState();
        final subscription = scrollController
            .positionAsStream()
            .pairwise()
            .map((p) => p.last > p.first)
            .distinct() // If direction don't change, skip it 
            .listen((down) => down ? hideFabAnimationController.forward() : hideFabAnimationController.reverse());
    }



    FadeTransition(
                opacity: hideFabAnimationController,
                child: ScaleTransition(
                  scale: hideFabAnimationController,
                  child: FloatingActionButton(
                    onPressed: () => {},
                    child: const Icon(Icons.add),
                  ),
                ),
              )

And don't forget to cancel the subscription!


      @override
      void dispose() {
        subscription.cancel();
      }

You can do other things like throttle the stream when users scroll way too fast.

The answer of @Josteve is correct, but it isn't a good idea to call setState() each time the users scrolls. A better approach would look like this:

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  ScrollController controller;
  bool _isFabVisible = true;

  @override
  void initState() {
    super.initState();
    controller = ScrollController();
    controller.addListener(() {
      // FAB should be visible if and only if user has not scrolled to bottom
      var userHasScrolledToBottom = controller.position.atEdge && controller.position.pixels > 0;

      if(_isFabVisible == userHasScrolledToBottom) {
        setState(() => _isFabVisible = !userHasScrolledToBottom);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        controller: controller,
        children: List.generate(
            100,
            (index) => ListTile(
                  title: Text("Text $index"),
                )),
      ),
      floatingActionButton: AnimatedOpacity(
        child: FloatingActionButton(
          child: Icon(Icons.add),
          tooltip: "Increment",
          onPressed: () {
            print("Pressed");
          },
        ),
        duration: Duration(milliseconds: 100),
        opacity: _isFabVisible? 1 : 0,
      ),
    );
  }
}
Related