diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart index eaaeca8619f..07a201c0f28 100644 --- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart +++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart @@ -10,6 +10,7 @@ import 'framework.dart'; import 'inherited_notifier.dart'; import 'layout_builder.dart'; import 'notification_listener.dart'; +import 'scroll_activity.dart'; import 'scroll_context.dart'; import 'scroll_controller.dart'; import 'scroll_notification.dart'; @@ -431,9 +432,17 @@ class _DraggableScrollableSheetScrollPosition ); VoidCallback? _dragCancelCallback; + VoidCallback? _ballisticCancelCallback; final _DraggableSheetExtent extent; bool get listShouldScroll => pixels > 0.0; + @override + void beginActivity(ScrollActivity? newActivity) { + // Cancel the running ballistic simulation, if there is one. + _ballisticCancelCallback?.call(); + super.beginActivity(newActivity); + } + @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { // We need to provide some extra extent if we haven't yet reached the max or @@ -481,6 +490,9 @@ class _DraggableScrollableSheetScrollPosition debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), vsync: context.vsync, ); + // Stop the ballistic animation if a new activity starts. + // See: [beginActivity]. + _ballisticCancelCallback = ballisticController.stop; double lastDelta = 0; void _tick() { final double delta = ballisticController.value - lastDelta; @@ -501,7 +513,10 @@ class _DraggableScrollableSheetScrollPosition ballisticController ..addListener(_tick) ..animateWith(simulation).whenCompleteOrCancel( - ballisticController.dispose, + () { + _ballisticCancelCallback = null; + ballisticController.dispose(); + }, ); } diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart index e9e6adb6c76..db718670d6a 100644 --- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart +++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart @@ -264,6 +264,40 @@ void main() { expect(find.text('Item 70'), findsNothing); }, variant: TargetPlatformVariant.all()); + testWidgets('Ballistic animation on fling can be interrupted', (WidgetTester tester) async { + int taps = 0; + await tester.pumpWidget(_boilerplate(() => taps++)); + + expect(find.text('TapHere'), findsOneWidget); + await tester.tap(find.text('TapHere')); + expect(taps, 1); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 31'), findsNothing); + expect(find.text('Item 70'), findsNothing); + + await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000); + // Don't pump and settle because we want to interrupt the ballistic scrolling animation. + expect(find.text('TapHere'), findsOneWidget); + await tester.tap(find.text('TapHere'), warnIfMissed: false); + expect(taps, 2); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 31'), findsOneWidget); + expect(find.text('Item 70'), findsNothing); + + // Use `dragFrom` here because calling `drag` on a list item without + // first calling `pumpAndSettle` fails with a hit test error. + await tester.dragFrom(const Offset(0, 200), const Offset(0, 200)); + await tester.pumpAndSettle(); + + // Verify that the ballistic animation has canceled and the sheet has + // returned to it's original position. + await tester.tap(find.text('TapHere')); + expect(taps, 3); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 31'), findsNothing); + expect(find.text('Item 70'), findsNothing); + }, variant: TargetPlatformVariant.all()); + debugDefaultTargetPlatformOverride = null; });