From 212e3f4e2e20220d5ea2929405d33e83228deec7 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 11 May 2021 17:24:02 -0500 Subject: [PATCH] More mouse tweaks (#82293) --- .../flutter/lib/src/material/scrollbar.dart | 2 +- .../flutter/lib/src/widgets/scrollbar.dart | 45 ++++++++++---- .../flutter/test/widgets/scrollbar_test.dart | 61 +++++++++++++++++++ 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index fdcc3a9111..6f68528671 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -399,7 +399,7 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { void handleHover(PointerHoverEvent event) { super.handleHover(event); // Check if the position of the pointer falls over the painted scrollbar - if (isPointerOverScrollbar(event.position, event.kind)) { + if (isPointerOverScrollbar(event.position, event.kind, forHover: true)) { // Pointer is hovering over the scrollbar setState(() { _hoverIsActive = true; }); _hoverAnimationController.forward(); diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 2b24764676..248a1dc0ed 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -481,22 +481,35 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// Same as hitTest, but includes some padding when the [PointerEvent] is /// caused by [PointerDeviceKind.touch] to make sure that the region /// isn't too small to be interacted with by the user. - bool hitTestInteractive(Offset position, PointerDeviceKind kind) { + /// + /// The hit test area for hovering with [PointerDeviceKind.mouse] over the + /// scrollbar also uses this extra padding. This is to make it easier to + /// interact with the scrollbar by presenting it to the mouse for interaction + /// based on proximity. When `forHover` is true, the larger hit test area will + /// be used. + bool hitTestInteractive(Offset position, PointerDeviceKind kind, { bool forHover = false }) { if (_thumbRect == null) { - return false; - } - // The scrollbar is not able to be hit when transparent. - if (fadeoutOpacityAnimation.value == 0.0) { + // We have never painted the scrollbar, so we do not know where it will be. return false; } final Rect interactiveRect = _trackRect ?? _thumbRect!; + final Rect paddedRect = interactiveRect.expandToInclude( + Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2), + ); + + // The scrollbar is not able to be hit when transparent - except when + // hovering with a mouse. This should bring the scrollbar into view so the + // mouse can interact with it. + if (fadeoutOpacityAnimation.value == 0.0) { + if (forHover && kind == PointerDeviceKind.mouse) + return paddedRect.contains(position); + return false; + } + switch (kind) { case PointerDeviceKind.touch: - final Rect touchScrollbarRect = interactiveRect.expandToInclude( - Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2), - ); - return touchScrollbarRect.contains(position); + return paddedRect.contains(position); case PointerDeviceKind.mouse: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: @@ -1280,13 +1293,18 @@ class RawScrollbarState extends State with TickerProv } /// Returns true if the provided [Offset] is located over the track or thumb /// of the [RawScrollbar]. + /// + /// The hit test area for mouse hovering over the scrollbar is larger than + /// regular hit testing. This is to make it easier to interact with the + /// scrollbar and present it to the mouse for interaction based on proximity. + /// When `forHover` is true, the larger hit test area will be used. @protected - bool isPointerOverScrollbar(Offset position, PointerDeviceKind kind) { + bool isPointerOverScrollbar(Offset position, PointerDeviceKind kind, { bool forHover = false }) { if (_scrollbarPainterKey.currentContext == null) { return false; } final Offset localOffset = _getLocalOffset(_scrollbarPainterKey, position); - return scrollbarPainter.hitTestInteractive(localOffset, kind); + return scrollbarPainter.hitTestInteractive(localOffset, kind, forHover: true); } /// Cancels the fade out animation so the scrollbar will remain visible for @@ -1301,8 +1319,11 @@ class RawScrollbarState extends State with TickerProv @mustCallSuper void handleHover(PointerHoverEvent event) { // Check if the position of the pointer falls over the painted scrollbar - if (isPointerOverScrollbar(event.position, event.kind)) { + if (isPointerOverScrollbar(event.position, event.kind, forHover: true)) { _hoverIsActive = true; + // Bring the scrollbar back into view if it has faded or started to fade + // away. + _fadeoutAnimationController.forward(); _fadeoutTimer?.cancel(); } else if (_hoverIsActive) { // Pointer is not over painted scrollbar. diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index 3a7158dca3..64845a6fc5 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -697,6 +697,67 @@ void main() { ); }); + testWidgets('Scrollbar will fade back in when hovering over known track area', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData(), + child: RawScrollbar( + child: SingleChildScrollView( + child: SizedBox(width: 4000.0, height: 4000.0), + ), + ), + ), + ), + ); + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView))); + await gesture.moveBy(const Offset(0.0, -20.0)); + await tester.pump(); + // Scrollbar fully showing + await tester.pump(const Duration(milliseconds: 500)); + expect( + find.byType(RawScrollbar), + paints + ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) + ..rect( + rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0), + color: const Color(0x66BCBCBC), + ), + ); + await gesture.up(); + await tester.pump(_kScrollbarTimeToFade); + await tester.pump(_kScrollbarFadeDuration * 0.5); + + // Scrollbar is fading out + expect( + find.byType(RawScrollbar), + paints + ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) + ..rect( + rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0), + color: const Color(0x4fbcbcbc), + ), + ); + + // Hover over scrollbar with mouse to bring opacity back up + final TestGesture mouseGesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); + await mouseGesture.addPointer(); + addTearDown(mouseGesture.removePointer); + await mouseGesture.moveTo(const Offset(794.0, 5.0)); + await tester.pumpAndSettle(); + // Scrollbar should be visible + expect( + find.byType(RawScrollbar), + paints + ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) + ..rect( + rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0), + color: const Color(0x66BCBCBC), + ), + ); + }); + testWidgets('Scrollbar thumb can be dragged', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget(