Flutter - Hiding FloatingActionButton
Asked Answered
E

10

69

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

Endurant answered 11/8, 2017 at 9:15 Comment(0)
B
92
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(

        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
 }
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
 }

 class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  ScrollController _hideButtonController;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  var _isVisible;
  @override
  initState(){
    super.initState();
    _isVisible = true;
    _hideButtonController = new ScrollController();
    _hideButtonController.addListener((){
      if(_hideButtonController.position.userScrollDirection == ScrollDirection.reverse){
        if(_isVisible == true) {
            /* only set when the previous state is false
             * Less widget rebuilds 
             */
            print("**** ${_isVisible} up"); //Move IO away from setState
            setState((){
              _isVisible = false;
            });
        }
      } else {
        if(_hideButtonController.position.userScrollDirection == ScrollDirection.forward){
          if(_isVisible == false) {
              /* only set when the previous state is false
               * Less widget rebuilds 
               */
               print("**** ${_isVisible} down"); //Move IO away from setState
               setState((){
                 _isVisible = true;
               });
           }
        }
    }});
  }
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new CustomScrollView(
          controller: _hideButtonController,
          shrinkWrap: true,
          slivers: <Widget>[
            new SliverPadding(
              padding: const EdgeInsets.all(20.0),
              sliver: new SliverList(
                delegate: new SliverChildListDelegate(
                  <Widget>[
                    const Text('I\'m dedicating every day to you'),
                    const Text('Domestic life was never quite my style'),
                    const Text('When you smile, you knock me out, I fall apart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('And I thought I was so smart'),
                    const Text('I realize I am crazy'),   
                  ],
                ),
              ),
            ),
          ],
        )
      ),
      floatingActionButton: new Visibility( 
        visible: _isVisible,
        child: new FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: new Icon(Icons.add),
        ),     
      ),
    );
  }
}

I apologize if I did not use listview since I do not know how to scroll with listview. I will answer the other parts of your question.

First you need to create a scrollcontroller that will listen scrollPostion events

If scrollcontroller manages to find either scrolldirection forward or reverse. You add a state that set a state to visible.

When you draw the button, you wrap the button in a visibility class. You set the visible flag and the widget should ignore input commands.

Edit: I cant seem to add links to ScrollController, ScrollerPosition, ScrollDirection, and Opacity. I guess you can search it yourself or somebody else edit in the links

Edit2: Use CopsonRoad or use visibility widget, unless you want an unpainted widget in the layout tree

Edit3: In light of newcomers using code as is, I would update the code to encourage better practices. Use visibility instead of Opacity. Remove io from setState. tested on Flutter 1.5.4-hotfix.2

Behistun answered 15/8, 2017 at 21:51 Comment(11)
According to this flutter issue you have to use import 'package:flutter/rendering.dart'; in order for ScrollController to be usableSatinet
I included rendering.dartBehistun
Don't use Opacity. See Sibin's solution below for correct use of Visibility control.Moltke
@JimGomes Visibility wasnt an option when I wrote my answer github.com/flutter/flutter/pull/20365 It was added 9 months ago. CopsOnRoad answer is much cleaner than mineBehistun
@Behistun I prefer your solution, just swapping out Opacity to use Visibility instead. I suggest updating your example with that control. I've based my implementation on your solution.Moltke
My list scrolling gets slow when I use the listener with setstate, it hurt and much to ux, someone else with the same problem?Glennisglennon
@JustCase I would remove the print statement in the set state. I would avoid memory allocations and io in setStateBehistun
@Behistun Yes, I removed the print. My scrolling is slow when I have the controler on the list. I have not found a solution so far, I am studying the flutter perfomance documentation.Glennisglennon
@JustCase you are doing too much in setState. setState should be reserved for assignment only. My code above encourages bad practices. I decided to update my code. If your code is still slow, then your problem is somewhere elseBehistun
I prefer @vahid haj hosseini solution because it gives you built-in animation and is easy to useJennifferjennilee
Is it better to set the listener in initState or in build?Indebted
C
69

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')),
      ),
    ),
  );
}
Catalog answered 10/10, 2018 at 1:47 Comment(2)
this Visibility widget make my day, yup!Hibbs
To use animation you can simply use AnimatedOpacity instead of Visibility. Also better wrap AnimatedOpacity with IgnorePointer to make the button not clickableIndebted
G
35

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),
          ),
        ),
      ),
    );
  }
}
Gale answered 19/12, 2019 at 11:0 Comment(4)
Thank you for this solution! One small issue though. When I open a page with a FAB, it is hidden by default. To show it, I have to scroll down then up. Is there a way to show the FAB initially? ThanksGrendel
I think the easiest way to do is by just calling floatingActionButton: _showFab? MyFloatingActionButton() : null; and controlling the _showFab variable with the scrollController listener. This way you get build animationJennifferjennilee
@Grendel to answer your question, you can add _hideFabAnimation.forward() just after the AnimationController initialization in the initSateSinglecross
I like this solution. One addition that I used was to wrap my fab in AnimatedCrossFade which makes the animation implicit and I only have to change a bool flag.Glauconite
I
14

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,
      ),
    );
  }
}
Interlocutor answered 10/3, 2020 at 14:2 Comment(1)
not working with singleChildScrollView thoSteffin
A
12

you can use below code to keep default animation

floatingActionButton: _isVisible
        ? FloatingActionButton(...)
        : null,
Anaptyxis answered 3/2, 2020 at 6:24 Comment(1)
this should be the accepted answer! This gives you the built-in animation. Combine it with a scrollController listener which sets _isVisible with setStateJennifferjennilee
L
6

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

sample :

  floatingActionButton:
            Visibility(visible: _visibilityFlag , child: _buildFAB(context)),
Lietuva answered 13/3, 2019 at 6:27 Comment(1)
Visibility is what should be used instead of Opacity. With Opacity, the control is still there and can be activated even though you can't see it. This can lead to surprising results for the User, which is not good.Moltke
G
6

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),
          ),
        )
Guttate answered 5/9, 2019 at 19:0 Comment(0)
B
3

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 {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  late 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(
        duration: const Duration(milliseconds: 100),
        opacity: _isFabVisible? 1 : 0,
        child: FloatingActionButton(
          tooltip: "Increment",
          onPressed: () {
            debugPrint('Pressed');
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
Bork answered 26/9, 2021 at 18:22 Comment(1)
Non-nullable instance field 'controller' must be initialized. Try adding an initializer expression, or a generative constructor that initializes it, or mark it 'late'.dart(not_initialized_non_nullable_instance_field)Plectognath
I
1

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.

Ichthyology answered 3/1, 2022 at 10:55 Comment(0)
H
0
    final ValueNotifier<bool> _showFloatingButton = ValueNotifier<bool>(true);
 body: NotificationListener<UserScrollNotification>(
              onNotification: (notification) {
                final ScrollDirection direction = notification.direction;

                if (direction == ScrollDirection.reverse) {
                  _showFloatingButton.value = false;
                } else if (direction == ScrollDirection.forward) {
                  _showFloatingButton.value = true;
                }

                return true;
              },
  floatingActionButton: Visibility(
              visible: dashboardViewModel.getProfileLoader,
              child: Container(
                margin: const EdgeInsets.only(
                    bottom: 56.0), // Set your desired bottom margin here
                child: ValueListenableBuilder(
                    valueListenable: _showFloatingButton,
                    builder: ((context, value, child) {
                      debugPrint("Value listener ${_showFloatingButton.value}");
                      if (_showFloatingButton.value == true) {
                        return FloatingActionButton(
                          backgroundColor: Colors.indigo,
                          foregroundColor: Colors.white,
                          onPressed: () {
                            showProfileBottomSheet(context, dashboardViewModel);
                            // Provider.of<StatesViewModel>(context, listen: false)
                            //     .loadInitial(context);

                            // Navigator.pushNamed(context, RouteNames.statesScreen);
                          },
                          child: const Icon(Icons.add),
                          // Your text here
                        );
                      } else {
                        return Divider();
                      }
                    })),

if you want to avoid using set state you can check this code

Hollar answered 28/6, 2024 at 5:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.