Is there any built in way in Flutter to hide a FloatingActionButton
on ListView
scrolling down and then showing it on scrolling up?
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
initState
or in build
? –
Indebted 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:
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')),
),
),
);
}
Visibility
widget make my day, yup! –
Hibbs AnimatedOpacity
instead of Visibility
. Also better wrap AnimatedOpacity
with IgnorePointer
to make the button not clickable –
Indebted 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),
),
),
),
);
}
}
floatingActionButton: _showFab? MyFloatingActionButton() : null;
and controlling the _showFab
variable with the scrollController
listener. This way you get build animation –
Jennifferjennilee _hideFabAnimation.forward()
just after the AnimationController
initialization in the initSate
–
Singlecross 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,
scrollController
listener which sets _isVisible
with setState
–
Jennifferjennilee 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),
),
)
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),
),
),
);
}
}
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.
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
© 2022 - 2025 — McMap. All rights reserved.
import 'package:flutter/rendering.dart';
in order for ScrollController to be usable – Satinet