I have a view that consists of a Scaffold
and a single ListView
in its body, each children of the list is a different widget that represents various "sections" of the view (sections range from simple TextViews to arrangements of Column
s and Row
s), I want to show a FloatingActionButon
only when the user scrolls over certain Widgets
(which aren't initially visible due to being far down the list).

- 8,354
- 9
- 38
- 66
-
maybe [this](https://medium.com/flutter-io/managing-visibility-in-flutter-f558588adefe) will help you to get something – Raouf Rahiche Jun 27 '18 at 21:37
-
in addition, see this https://stackoverflow.com/questions/44331725/onresume-and-onpause-for-widgets-on-flutter – AbdulMomen عبدالمؤمن Mar 25 '19 at 07:02
-
2You can use this [package](https://pub.dev/packages/inview_notifier_list) – Vamsi Krishna Jun 19 '19 at 09:17
-
You could try the one below - https://stackoverflow.com/a/57252652/1711454 using `flutter_widgets` which was not available back then has this feature now – Artur Stepniewski Jul 30 '19 at 09:31
3 Answers
https://pub.dev/packages/visibility_detector provides this functionality with its VisibilityDetector
widget that can wrap any other Widget
and notify when the visible area of the widget changed:
VisibilityDetector(
key: Key("unique key"),
onVisibilityChanged: (VisibilityInfo info) {
debugPrint("${info.visibleFraction} of my widget is visible");
},
child: MyWidgetToTrack());
)

- 1,401
- 2
- 11
- 10
-
34Does anyone know what kind of performance hit `VisibilityDetector` takes? I want to wrap each item in a `ListView.builder` and when it's visible I want to log an impression event to Firebase Analytics. – Moshe G Apr 01 '20 at 14:53
-
-
4@MosheG this package is developed by Google so *I guess* it must be optimised. – Slick Slime Mar 09 '21 at 06:06
-
I haven't had to go back to flutter in a while, but will try this and possibly change the accepted answer! – arielnmz Aug 22 '21 at 06:45
-
It doesn't perform well with videos, aspect ratio + vid has a pop in buggy effect when wrapped with one of these – Petro Sep 14 '21 at 05:26
-
-
I used this package for messenger messages to check if the user sees the message and update the database, it works great – Mohsen Haydari Nov 02 '22 at 05:58
With the rephrased question, I have a clearer understanding about what you're trying to do. You have a list of widgets, and want to decide whether to show a floating action button based on whether those widgets are currently being shown in the viewport.
I've written a basic example which shows this in action. I'll describe the various elements below, but please be aware that:
- It uses a GlobalKey which tends not to be overly efficient
- It runs continuously and does some non-optimal calculations each and every frame during scroll.
Therefore, it might cause your app to slow down. I'll leave it to someone else to optimize or write a better answer that uses a better knowledge of the render tree to do this same thing.
Anyways, here's the code. I'll first give you the relatively more naive way of doing it - using setState on a variable directly, as it's simpler:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
GlobalKey<State> key = new GlobalKey();
double fabOpacity = 1.0;
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Scrolling."),
),
body: NotificationListener<ScrollNotification>(
child: new ListView(
itemExtent: 100.0,
children: [
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
new MyObservableWidget(key: key),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder()
],
),
onNotification: (ScrollNotification scroll) {
var currentContext = key.currentContext;
if (currentContext == null) return false;
var renderObject = currentContext.findRenderObject();
RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
var offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0);
var offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0);
if (offsetToRevealBottom.offset > scroll.metrics.pixels ||
scroll.metrics.pixels > offsetToRevealTop.offset) {
if (fabOpacity != 0.0) {
setState(() {
fabOpacity = 0.0;
});
}
} else {
if (fabOpacity == 0.0) {
setState(() {
fabOpacity = 1.0;
});
}
}
return false;
},
),
floatingActionButton: new Opacity(
opacity: fabOpacity,
child: new FloatingActionButton(
onPressed: () {
print("YAY");
},
),
),
),
);
}
}
class MyObservableWidget extends StatefulWidget {
const MyObservableWidget({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => new MyObservableWidgetState();
}
class MyObservableWidgetState extends State<MyObservableWidget> {
@override
Widget build(BuildContext context) {
return new Container(height: 100.0, color: Colors.green);
}
}
class ContainerWithBorder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(border: new Border.all(), color: Colors.grey),
);
}
}
There's a few easily fixable issues with this - it doesn't hide the button but just makes it transparent, it renders the entire widget each time, and it does the calculations for the position of the widget each frame.
This is a more optimized version, where it doesn't do the calculations if it doesn't need to. You might need to add more logic to it if your list ever changes (or you could just do the calculations each time and if the performance is good enough not worry about it). Note how it uses an animationController and AnimatedBuilder to make sure that only the relevant part builds each time. You could also get rid of the fading in/fading out by simply setting the animationController's value
directly and doing the opacity calculation yourself (i.e. you may want it to get opaque as it starts scrolling into view, which would have to take into account the height of your object):
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> with TickerProviderStateMixin<MyApp> {
GlobalKey<State> key = new GlobalKey();
bool fabShowing = false;
// non-state-managed variables
AnimationController _controller;
RenderObject _prevRenderObject;
double _offsetToRevealBottom = double.infinity;
double _offsetToRevealTop = double.negativeInfinity;
@override
void initState() {
super.initState();
_controller = new AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_controller.addStatusListener((val) {
if (val == AnimationStatus.dismissed) {
setState(() => fabShowing = false);
}
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Scrolling."),
),
body: NotificationListener<ScrollNotification>(
child: new ListView(
itemExtent: 100.0,
children: [
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
new MyObservableWidget(key: key),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder()
],
),
onNotification: (ScrollNotification scroll) {
var currentContext = key.currentContext;
if (currentContext == null) return false;
var renderObject = currentContext.findRenderObject();
if (renderObject != _prevRenderObject) {
RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
_offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0).offset;
_offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0).offset;
}
final offset = scroll.metrics.pixels;
if (_offsetToRevealBottom < offset && offset < _offsetToRevealTop) {
if (!fabShowing) setState(() => fabShowing = true);
if (_controller.status != AnimationStatus.forward) {
_controller.forward();
}
} else {
if (_controller.status != AnimationStatus.reverse) {
_controller.reverse();
}
}
return false;
},
),
floatingActionButton: fabShowing
? new AnimatedBuilder(
child: new FloatingActionButton(
onPressed: () {
print("YAY");
},
),
builder: (BuildContext context, Widget child) => Opacity(opacity: _controller.value, child: child),
animation: this._controller,
)
: null,
),
);
}
}
class MyObservableWidget extends StatefulWidget {
const MyObservableWidget({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => new MyObservableWidgetState();
}
class MyObservableWidgetState extends State<MyObservableWidget> {
@override
Widget build(BuildContext context) {
return new Container(height: 100.0, color: Colors.green);
}
}
class ContainerWithBorder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(border: new Border.all(), color: Colors.grey),
);
}
}

- 37,718
- 9
- 112
- 99
Here is the simplest approach, only three lines of code!
I don't guarantee it will work, but it is worth a try.
Precondition: You have to have the Key
of the widget you're checking the visibility for.
final RenderObject? box = _widgetKey.currentContext?.findRenderObject(); // !
if (box != null) {
final double yPosition = (box as RenderBox).localToGlobal(Offset.zero).dy; // !
print('Widget is visible in the viewport at position: $yPosition');
// do stuff...
}
else {
print('Widget is not visible.');
// do stuff...
}

- 3,558
- 1
- 39
- 42
-
this doesn't work reliably. F.e. an item in a listview can have a renderobject, but still be inactive and not shown. – user3249027 Jan 07 '22 at 21:54
-
@user3249027 I wrote "I don't guarantee it will work, but it is worth a try." So You can try it and not be surprised if it doesn't work. :D However, it worked for me and for some other people as well.. can You give Your example where it didn't work, so I can add that to the answer? – Aleksandar Jan 09 '22 at 10:43
-
@user3249027 also, as You can see, I am calculating the `yPosition` if a `RenderObject` is present. Maybe You can use that (offset on the y-axis) and compare it to the screen height to check if the widget is visible in the screen. Also, whether this will work depends on Your specific case, i.e. current layout and moment of the execution of the snippet above. – Aleksandar Jan 09 '22 at 10:52
-
hey @Aleksandar you are right, I'm talking about a special use case; came across this while working with a `ListView.builder`: that widgets builds its children lazily and "precached", where some children pre & post the currently visible children are created - and therefore "have" a `renderObject` - but are marked (bc off screen) as `"inactive"`. And calling `.localToGlobal()` on an inactive `renderObject` will throw an exception. – user3249027 Jan 11 '22 at 11:20
-
@user3249027 I guess You found Your way of telling if the widgets are visible, congrats! If You think I should add something to my answer, please let me know. – Aleksandar Jan 11 '22 at 14:56