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).
Flutter Visibility Widget Visibility is a widget that is useful to show or hide other widgets in flutter. We have to wrap the widget we want to show or hide inside the visibility widget as a child. This widget has a property called visible which controls the state (visible or invisible) of the child.
Viewport is the visual workhorse of the scrolling machinery. It displays a subset of its children according to its own dimensions and the given offset. As the offset varies, different children are visible through the viewport.
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()); )
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:
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), ); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With