Cancel DraggableScrollableSheet ballistic animation if a new activity begins (#86614)
diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
index eaaeca8..07a201c 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 '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,10 +432,18 @@
);
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
// min extents. Otherwise, a list with fewer children than the extent of
@@ -481,6 +490,9 @@
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 @@
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 e9e6adb..db71867 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 @@
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;
});