halkyon/packages/halkyon_ui/lib/src/widget/scroll_hiding_fab.dart

76 lines
2.8 KiB
Dart
Raw Normal View History

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<ScrollHidingFab> createState() => _ScrollHidingFabState();
}
class _ScrollHidingFabState() extends State<ScrollHidingFab> {
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<UserScrollNotification>(
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),
),
),
),
);
}
}