import 'package:flutter/rendering.dart'; import 'package:material_ui/material_ui.dart'; /// A floating action button that slides away while the person scrolls down, /// reading onwards, and comes back as soon as they scroll up. [builder] /// builds the screen around the button it is handed (as its scaffold's /// floating action button); the scrolling in that screen moves it. /// /// While hidden, the button can't be tapped through its fading ghost. A list /// short enough not to scroll keeps it in view: it covers nothing there, and /// so does [pinned]. class const ScrollHidingFab({ /// The button; none when null, as in windows that put the action in the /// app bar instead. required final Widget? fab, /// The screen, given the button to place. required final Widget Function(BuildContext context, Widget? fab) builder, /// Whether the button stays whatever the scrolling, as while it is a field /// being written in, which mustn't slide away under the person's typing. final bool pinned = false, super.key, }) extends StatefulWidget { @override State createState() => _ScrollHidingFabState(); } class _ScrollHidingFabState() extends State { var _visible = true; bool _onScroll(UserScrollNotification notification) { // Only the screen's own vertical scrolling: a sideways swipe between // tabs moves nothing. if (notification.metrics.axis != Axis.vertical || widget.pinned) { return false; } final visible = switch (notification.direction) { // A list that doesn't scroll covers nothing behind the button, and an // overscroll bounce there mustn't hide it. ScrollDirection.reverse => notification.metrics.maxScrollExtent <= 0, ScrollDirection.forward => true, ScrollDirection.idle => _visible, }; if (visible != _visible) { setState(() => _visible = visible); } return false; } @override Widget build(BuildContext context) { final fab = widget.fab; final visible = _visible || widget.pinned; return NotificationListener( onNotification: _onScroll, child: widget.builder( context, fab == null ? null : AnimatedSlide( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, // Twice its own height clears the window's edge whatever // the button's size. offset: visible ? Offset.zero : const Offset(0, 2), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: visible ? 1 : 0, child: IgnorePointer(ignoring: !visible, child: fab), ), ), ), ); } }