diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 921657dd87d..5f63ccc62e1 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -16,6 +16,7 @@ import 'gesture_detector.dart'; import 'media_query.dart'; import 'notification_listener.dart'; import 'primary_scroll_controller.dart'; +import 'scroll_configuration.dart'; import 'scroll_controller.dart'; import 'scroll_metrics.dart'; import 'scroll_notification.dart'; @@ -1367,7 +1368,24 @@ class RawScrollbarState extends State with TickerProv if (scrollOffsetGlobal != position.pixels) { // Ensure we don't drag into overscroll if the physics do not allow it. final double physicsAdjustment = position.physics.applyBoundaryConditions(position, scrollOffsetGlobal); - position.jumpTo(scrollOffsetGlobal - physicsAdjustment); + double newPosition = scrollOffsetGlobal - physicsAdjustment; + + // The physics may allow overscroll when actually *scrolling*, but + // dragging on the scrollbar does not always allow us to enter overscroll. + switch(ScrollConfiguration.of(context).getPlatform(context)) { + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + newPosition = newPosition.clamp(0.0, position.maxScrollExtent); + break; + case TargetPlatform.iOS: + case TargetPlatform.android: + // We can only drag the scrollbar into overscroll on mobile + // platforms, and only if the physics allow it. + break; + } + position.jumpTo(newPosition); } } diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index b8b2dec544d..22d4fe7d32b 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -1025,6 +1025,139 @@ void main() { ); }); + testWidgets('Scrollbar thumb cannot be dragged into overscroll if the platform does not allow it', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + // Don't apply a scrollbar automatically for this test. + behavior: const ScrollBehavior().copyWith( + scrollbars: false, + physics: const AlwaysScrollableScrollPhysics(), + ), + child: PrimaryScrollController( + controller: scrollController, + child: RawScrollbar( + isAlwaysShown: true, + controller: scrollController, + child: const SingleChildScrollView( + child: SizedBox(width: 4000.0, height: 4000.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, 90.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Try to drag the thumb into overscroll. + const double scrollAmount = -10.0; + final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0)); + await tester.pumpAndSettle(); + await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount)); + await tester.pumpAndSettle(); + + // The platform drag handling should not have allowed us to enter overscroll. + 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, 90.0), + color: const Color(0x66BCBCBC), + ), + ); + + await dragScrollbarGesture.up(); + await tester.pumpAndSettle(); + }, variant: const TargetPlatformVariant({ + TargetPlatform.macOS, + TargetPlatform.linux, + TargetPlatform.windows, + TargetPlatform.fuchsia, + })); + + testWidgets('Scrollbar thumb can be dragged into overscroll if the platform allows it', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + // Don't apply a scrollbar automatically for this test. + behavior: const ScrollBehavior().copyWith( + scrollbars: false, + physics: const AlwaysScrollableScrollPhysics(), + ), + child: PrimaryScrollController( + controller: scrollController, + child: RawScrollbar( + isAlwaysShown: true, + controller: scrollController, + child: const SingleChildScrollView( + child: SizedBox(width: 4000.0, height: 4000.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, 90.0), + color: const Color(0x66BCBCBC), + ), + ); + + // Try to drag the thumb into overscroll. + const double scrollAmount = -10.0; + final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0)); + await tester.pumpAndSettle(); + await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount)); + await tester.pumpAndSettle(); + + // The platform drag handling should have allowed us to enter overscroll. + expect(scrollController.offset, lessThan(-66.0)); + expect( + find.byType(RawScrollbar), + paints + ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) + ..rect( + // The size of the scrollbar thumb shrinks when overscrolling + rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 80.0), + color: const Color(0x66BCBCBC), + ), + ); + + await dragScrollbarGesture.up(); + await tester.pumpAndSettle(); + }, variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + })); + // Regression test for https://github.com/flutter/flutter/issues/66444 testWidgets("RawScrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey();