From fc8546345ea993650443498598fd46acb96dc124 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 29 May 2021 18:46:11 +0200 Subject: [PATCH] Show arrow indicator on toolbar When the number of items in the toolbar is very large and the toolbar becomes scrollable, arrows are now displayed to indicate to the user that the list is scrollable. The arrows are automatically hidden if it is not scrollable in the respective arrow direction. --- lib/src/widgets/toolbar.dart | 15 +-- .../toolbar/arrow_indicated_button_list.dart | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 lib/src/widgets/toolbar/arrow_indicated_button_list.dart diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d4d00f5d..6b3a5e8c 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart'; import '../models/documents/attribute.dart'; import 'controller.dart'; +import 'toolbar/arrow_indicated_button_list.dart'; import 'toolbar/clear_format_button.dart'; import 'toolbar/color_button.dart'; import 'toolbar/history_button.dart'; @@ -316,21 +317,9 @@ class _QuillToolbarState extends State { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), color: Theme.of(context).canvasColor, - child: CustomScrollView( - scrollDirection: Axis.horizontal, - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: widget.children, - ), - ), - ], - ), + child: ArrowIndicatedButtonList(buttons: widget.children), ); } } diff --git a/lib/src/widgets/toolbar/arrow_indicated_button_list.dart b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart new file mode 100644 index 00000000..b17157d4 --- /dev/null +++ b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Scrollable list with arrow indicators. +/// +/// The arrow indicators are automatically hidden if the list is not +/// scrollable in the direction of the respective arrow. +class ArrowIndicatedButtonList extends StatefulWidget { + const ArrowIndicatedButtonList({required this.buttons, Key? key}) + : super(key: key); + + final List buttons; + + @override + _ArrowIndicatedButtonListState createState() => + _ArrowIndicatedButtonListState(); +} + +class _ArrowIndicatedButtonListState extends State + with WidgetsBindingObserver { + final ScrollController _controller = ScrollController(); + bool _showLeftArrow = false; + bool _showRightArrow = false; + + @override + void initState() { + super.initState(); + _controller.addListener(_handleScroll); + + // Listening to the WidgetsBinding instance is necessary so that we can + // hide the arrows when the window gets a new size and thus the toolbar + // becomes scrollable/unscrollable. + WidgetsBinding.instance!.addObserver(this); + + // Workaround to allow the scroll controller attach to our ListView so that + // we can detect if overflow arrows need to be shown on init. + Timer.run(_handleScroll); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _buildLeftArrow(), + _buildScrollableList(), + _buildRightColor(), + ], + ); + } + + @override + void didChangeMetrics() => _handleScroll(); + + @override + void dispose() { + _controller.dispose(); + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } + + void _handleScroll() { + setState(() { + _showLeftArrow = + _controller.position.minScrollExtent != _controller.position.pixels; + _showRightArrow = + _controller.position.maxScrollExtent != _controller.position.pixels; + }); + } + + Widget _buildLeftArrow() { + return SizedBox( + width: 8, + child: Transform.translate( + // Move the icon a few pixels to center it + offset: const Offset(-5, 0), + child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null, + ), + ); + } + + Widget _buildScrollableList() { + return Expanded( + child: ScrollConfiguration( + // Remove the glowing effect, as we already have the arrow indicators + behavior: _NoGlowBehavior(), + // The CustomScrollView is necessary so that the children are not + // stretched to the height of the toolbar, https://bit.ly/3uC3bjI + child: CustomScrollView( + scrollDirection: Axis.horizontal, + controller: _controller, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: widget.buttons, + ), + ) + ], + ), + ), + ); + } + + Widget _buildRightColor() { + return SizedBox( + width: 8, + child: Transform.translate( + // Move the icon a few pixels to center it + offset: const Offset(-5, 0), + child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null, + ), + ); + } +} + +/// ScrollBehavior without the Material glow effect. +class _NoGlowBehavior extends ScrollBehavior { + @override + Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) { + return child; + } +}