From ff6aa928aa0982aedd6c008cf53bbad7baff687f Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 19 Sep 2022 10:51:16 +0300 Subject: [PATCH] Fix `Scrollbar` thumb drag behavior on desktop. (#111250) --- .../flutter/lib/src/widgets/scrollbar.dart | 24 ++- .../flutter/test/widgets/scrollbar_test.dart | 138 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 00cd2a7407d..15b38743326 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -1447,6 +1447,7 @@ class RawScrollbar extends StatefulWidget { /// scrollbar track. class RawScrollbarState extends State with TickerProviderStateMixin { Offset? _dragScrollbarAxisOffset; + late double? _thumbPress; ScrollController? _currentController; Timer? _fadeoutTimer; late AnimationController _fadeoutAnimationController; @@ -1785,6 +1786,9 @@ class RawScrollbarState extends State with TickerProv _fadeoutTimer?.cancel(); _fadeoutAnimationController.forward(); _dragScrollbarAxisOffset = localPosition; + _thumbPress = direction == Axis.vertical + ? localPosition.dy - scrollbarPainter._thumbOffset + : localPosition.dx - scrollbarPainter._thumbOffset; } /// Handler called when a currently active long press gesture moves. @@ -1802,10 +1806,28 @@ class RawScrollbarState extends State with TickerProv if (direction == null) { return; } - _updateScrollPosition(localPosition); + switch (position.axisDirection) { + case AxisDirection.up: + case AxisDirection.down: + if (_canDragThumb(_dragScrollbarAxisOffset!.dy, position.viewportDimension, _thumbPress!)) { + _updateScrollPosition(localPosition); + } + break; + case AxisDirection.left: + case AxisDirection.right: + if (_canDragThumb(_dragScrollbarAxisOffset!.dx, position.viewportDimension, _thumbPress!)) { + _updateScrollPosition(localPosition); + } + break; + } _dragScrollbarAxisOffset = localPosition; } + bool _canDragThumb(double dragOffset, double viewport, double thumbPress) { + return dragOffset >= thumbPress + && dragOffset <= viewport - (scrollbarPainter._thumbExtent - thumbPress); + } + /// Handler called when a long press has ended. @protected @mustCallSuper diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index 2c320426e7b..2d0c86d0b6d 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -2717,4 +2717,142 @@ void main() { expect(scrollController.offset, 0.0); }); + + testWidgets('Scrollbar thumb can only be dragged from long press point', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/107765 + + final ScrollController scrollController = ScrollController(); + final UniqueKey uniqueKey = UniqueKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith( + scrollbars: false, + ), + child: PrimaryScrollController( + controller: scrollController, + child: RawScrollbar( + isAlwaysShown: true, + controller: scrollController, + child: CustomScrollView( + primary: true, + slivers: [ + SliverToBoxAdapter( + child: Container( + height: 600.0, + ), + ), + SliverToBoxAdapter( + key: uniqueKey, + child: Container( + height: 600.0, + ), + ), + SliverToBoxAdapter( + child: Container( + height: 600.0, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(scrollController.offset, 0.0); + 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, 0.0, 800.0, 200.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Long press on the thumb in the center and drag down to the bottom. + const double scrollAmount = 400.0; + final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 100.0)); + await tester.pumpAndSettle(); + await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount)); + await tester.pumpAndSettle(); + + // Drag down past the long press point. + await dragScrollbarGesture.moveBy(const Offset(0.0, 100)); + await tester.pumpAndSettle(); + + // Drag up without reaching press point on the thumb. + await dragScrollbarGesture.moveBy(const Offset(0.0, -50)); + await tester.pumpAndSettle(); + + // Thumb should not move yet. + 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, 400.0, 800.0, 600.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Drag up to reach press point on the thumb. + await dragScrollbarGesture.moveBy(const Offset(0.0, -50)); + await tester.pumpAndSettle(); + + // Drag up. + await dragScrollbarGesture.moveBy(const Offset(0.0, -300)); + await tester.pumpAndSettle(); + + // Thumb should be moved. + 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, 100.0, 800.0, 300.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Drag up to reach the top and exceed the long press point. + await dragScrollbarGesture.moveBy(const Offset(0.0, -200)); + await tester.pumpAndSettle(); + + // Drag down to reach the long press point. + await dragScrollbarGesture.moveBy(const Offset(0.0, 100)); + await tester.pumpAndSettle(); + + // Thumb should not move yet. + 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, 0.0, 800.0, 200.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Drag down past the long press point. + await dragScrollbarGesture.moveBy(const Offset(0.0, 100)); + await tester.pumpAndSettle(); + + // Thumb should be moved. + 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, 100.0, 800.0, 300.0), + color: const Color(0x66BCBCBC), + ), + ); + }, variant: TargetPlatformVariant.desktop()); }