diff --git a/package/lib/src/foundation/sheet_activity.dart b/package/lib/src/foundation/sheet_activity.dart index 21fc246f..21322980 100644 --- a/package/lib/src/foundation/sheet_activity.dart +++ b/package/lib/src/foundation/sheet_activity.dart @@ -88,14 +88,19 @@ abstract class SheetActivity { case > 0: // Prevents the sheet from being pushed off the screen by the keyboard. final correction = min(0.0, metrics.maxViewPixels - metrics.viewPixels); - owner.setPixels(oldPixels + correction); + owner + ..setPixels(oldPixels + correction) + ..didUpdateMetrics(); case < 0: // Appends the delta of the bottom inset (typically the keyboard height) // to keep the visual sheet position unchanged. - owner.setPixels( - min(oldPixels - deltaInsetBottom, owner.metrics.maxPixels), - ); + owner + ..setPixels(min( + oldPixels - deltaInsetBottom, + owner.metrics.maxPixels, + )) + ..didUpdateMetrics(); } owner.settle(); @@ -177,7 +182,9 @@ class AnimatedSheetActivity extends SheetActivity final newInsets = owner.metrics.viewportInsets; final oldInsets = oldViewportInsets ?? newInsets; final deltaInsetBottom = newInsets.bottom - oldInsets.bottom; - owner.setPixels(owner.metrics.pixels - deltaInsetBottom); + owner + ..setPixels(owner.metrics.pixels - deltaInsetBottom) + ..didUpdateMetrics(); // 2. If the animation is still running, we start a new linear animation // to bring the sheet position to the recalculated final position in the @@ -252,17 +259,27 @@ class DragSheetActivity extends SheetActivity } @override - void applyUserDragUpdate(Offset offset) { + void applyUserDragUpdate(SheetDragUpdateDetails details) { final physicsAppliedDelta = - owner.physics.applyPhysicsToOffset(offset.dy, owner.metrics); + owner.physics.applyPhysicsToOffset(details.deltaY, owner.metrics); if (physicsAppliedDelta != 0) { - owner.setPixels(owner.metrics.pixels + physicsAppliedDelta); + owner + ..setPixels(owner.metrics.pixels + physicsAppliedDelta) + ..didDragUpdateMetrics(details); + } + + final overflow = + owner.physics.computeOverflow(details.deltaY, owner.metrics); + if (overflow != 0) { + owner.didOverflowBy(overflow); } } @override - void applyUserDragEnd(Velocity velocity) { - owner.goBallistic(velocity.pixelsPerSecond.dy); + void applyUserDragEnd(SheetDragEndDetails details) { + owner + ..didDragEnd(details) + ..goBallistic(details.velocityY); } } @@ -298,7 +315,9 @@ mixin ControlledSheetActivityMixin on SheetActivity { void onAnimationTick() { if (mounted) { final oldPixels = owner.metrics.pixels; - owner.setPixels(oldPixels + controller.value - _lastAnimatedValue); + owner + ..setPixels(oldPixels + controller.value - _lastAnimatedValue) + ..didUpdateMetrics(); _lastAnimatedValue = controller.value; } } @@ -331,7 +350,9 @@ mixin UserControlledSheetActivityMixin final deltaInsetBottom = newInsets.bottom - oldInsets.bottom; // Appends the delta of the bottom inset (typically the keyboard height) // to keep the visual sheet position unchanged. - owner.setPixels(owner.metrics.pixels - deltaInsetBottom); + owner + ..setPixels(owner.metrics.pixels - deltaInsetBottom) + ..didUpdateMetrics(); // We don't call `goSettling` here because the user is still // manually controlling the sheet position. } diff --git a/package/lib/src/foundation/sheet_drag.dart b/package/lib/src/foundation/sheet_drag.dart index 2fbeb3ed..103af025 100644 --- a/package/lib/src/foundation/sheet_drag.dart +++ b/package/lib/src/foundation/sheet_drag.dart @@ -217,8 +217,8 @@ class SheetDragEndDetails extends SheetDragDetails { @internal abstract class SheetDragControllerTarget { VerticalDirection get dragAxisDirection; - void applyUserDragUpdate(Offset offset); - void applyUserDragEnd(Velocity velocity); + void applyUserDragUpdate(SheetDragUpdateDetails details); + void applyUserDragEnd(SheetDragEndDetails details); /// Returns the minimum number of pixels that the sheet being dragged /// will potentially consume for the given drag delta. @@ -318,7 +318,7 @@ class SheetDragController implements Drag, ScrollActivityDelegate { } _lastDetails = details; - _target!.applyUserDragEnd(details.velocity); + _target!.applyUserDragEnd(details); } /// Called by the [ScrollDragController] in [Drag.update]. @@ -349,7 +349,7 @@ class SheetDragController implements Drag, ScrollActivityDelegate { } _lastDetails = details; - _target!.applyUserDragUpdate(details.delta); + _target!.applyUserDragUpdate(details); } @override diff --git a/package/lib/src/foundation/sheet_extent.dart b/package/lib/src/foundation/sheet_extent.dart index bc73b31f..82a9048d 100644 --- a/package/lib/src/foundation/sheet_extent.dart +++ b/package/lib/src/foundation/sheet_extent.dart @@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -384,24 +383,20 @@ abstract class SheetExtent extends ChangeNotifier final isDragging = activity.status == SheetStatus.dragging; // TODO: Make more typesafe - switch ((wasDragging, isDragging)) { - case (true, true): - assert(currentDrag != null); - assert(activity is SheetDragControllerTarget); - currentDrag!.updateTarget(activity as SheetDragControllerTarget); - - case (true, false): - assert(currentDrag != null); - dispatchDragEndNotification(); - currentDrag!.dispose(); - currentDrag = null; - - case (false, true): - assert(currentDrag != null); - dispatchDragStartNotification(); - - case (false, false): - assert(currentDrag == null); + assert(() { + final wasActuallyDragging = + currentDrag != null && oldActivity is SheetDragControllerTarget; + final isActuallyDragging = + currentDrag != null && activity is SheetDragControllerTarget; + return wasDragging == wasActuallyDragging && + isDragging == isActuallyDragging; + }()); + + if (wasDragging && isDragging) { + currentDrag!.updateTarget(activity as SheetDragControllerTarget); + } else if (wasDragging && !isDragging) { + currentDrag!.dispose(); + currentDrag = null; } oldActivity.dispose(); @@ -436,10 +431,7 @@ abstract class SheetExtent extends ChangeNotifier } } - Drag drag( - DragStartDetails details, - VoidCallback dragCancelCallback, - ) { + Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { assert(currentDrag == null); final dragActivity = DragSheetActivity(); var startDetails = SheetDragStartDetails( @@ -465,6 +457,7 @@ abstract class SheetExtent extends ChangeNotifier motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold, ); beginActivity(dragActivity); + didDragStart(startDetails); return drag; } @@ -482,11 +475,6 @@ abstract class SheetExtent extends ChangeNotifier correctPixels(pixels); if (oldPixels != pixels) { notifyListeners(); - if (currentDrag?.lastDetails is SheetDragUpdateDetails) { - dispatchDragUpdateNotification(); - } else { - dispatchUpdateNotification(); - } } } @@ -521,80 +509,46 @@ abstract class SheetExtent extends ChangeNotifier } } - void dispatchUpdateNotification() { + void didUpdateMetrics() { if (metrics.hasDimensions) { - _dispatchNotification( - SheetUpdateNotification( - metrics: metrics, - status: status, - ), - ); + SheetUpdateNotification( + metrics: metrics, + status: status, + ).dispatch(context.notificationContext); } } - void dispatchDragStartNotification() { + void didDragStart(SheetDragStartDetails details) { assert(metrics.hasDimensions); - assert(currentDrag != null); - final details = currentDrag!.lastDetails; - assert(details is SheetDragStartDetails); - _dispatchNotification( - SheetDragStartNotification( - metrics: metrics, - dragDetails: details as SheetDragStartDetails, - ), - ); + SheetDragStartNotification( + metrics: metrics, + dragDetails: details, + ).dispatch(context.notificationContext); } - void dispatchDragEndNotification() { + void didDragEnd(SheetDragEndDetails details) { assert(metrics.hasDimensions); - assert(currentDrag != null); - final details = currentDrag!.lastDetails; - assert(details is SheetDragEndDetails); - _dispatchNotification( - SheetDragEndNotification( - metrics: metrics, - dragDetails: details as SheetDragEndDetails, - ), - ); + SheetDragEndNotification( + metrics: metrics, + dragDetails: details, + ).dispatch(context.notificationContext); } - void dispatchDragUpdateNotification() { + void didDragUpdateMetrics(SheetDragUpdateDetails details) { assert(metrics.hasDimensions); - assert(currentDrag != null); - final details = currentDrag!.lastDetails; - assert(details is SheetDragUpdateDetails); - _dispatchNotification( - SheetDragUpdateNotification( - metrics: metrics, - dragDetails: details as SheetDragUpdateDetails, - ), - ); + SheetDragUpdateNotification( + metrics: metrics, + dragDetails: details, + ).dispatch(context.notificationContext); } - void dispatchOverflowNotification(double overflow) { + void didOverflowBy(double overflow) { assert(metrics.hasDimensions); - _dispatchNotification( - SheetOverflowNotification( - metrics: metrics, - status: status, - overflow: overflow, - ), - ); - } - - void _dispatchNotification(SheetNotification notification) { - // Avoid dispatching a notification in the middle of a build. - switch (SchedulerBinding.instance.schedulerPhase) { - case SchedulerPhase.postFrameCallbacks: - notification.dispatch(context.notificationContext); - case SchedulerPhase.idle: - case SchedulerPhase.midFrameMicrotasks: - case SchedulerPhase.persistentCallbacks: - case SchedulerPhase.transientCallbacks: - SchedulerBinding.instance.addPostFrameCallback((_) { - notification.dispatch(context.notificationContext); - }); - } + SheetOverflowNotification( + metrics: metrics, + status: status, + overflow: overflow, + ).dispatch(context.notificationContext); } String _debugMessage(String message) { diff --git a/package/lib/src/foundation/sheet_physics.dart b/package/lib/src/foundation/sheet_physics.dart index 7b1197a7..3f6dfa97 100644 --- a/package/lib/src/foundation/sheet_physics.dart +++ b/package/lib/src/foundation/sheet_physics.dart @@ -64,6 +64,7 @@ abstract class SheetPhysics { double computeOverflow(double offset, SheetMetrics metrics); + // TODO: Change to return a tuple of (physicsAppliedOffset, overflow) to avoid recomputation of the overflow. double applyPhysicsToOffset(double offset, SheetMetrics metrics); Simulation? createBallisticSimulation(double velocity, SheetMetrics metrics); @@ -92,6 +93,7 @@ mixin SheetPhysicsMixin on SheetPhysics { @override double applyPhysicsToOffset(double offset, SheetMetrics metrics) { + // TODO: Use computeOverflow() to calculate the overflowed pixels. if (parent case final parent?) { return parent.applyPhysicsToOffset(offset, metrics); } else if (offset > 0 && metrics.pixels < metrics.maxPixels) { diff --git a/package/lib/src/navigation/navigation_sheet_extent.dart b/package/lib/src/navigation/navigation_sheet_extent.dart index bdf38a50..cd197fb6 100644 --- a/package/lib/src/navigation/navigation_sheet_extent.dart +++ b/package/lib/src/navigation/navigation_sheet_extent.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; +import '../foundation/sheet_drag.dart'; import '../foundation/sheet_extent.dart'; import '../internal/transition_observer.dart'; import 'navigation_route.dart'; @@ -102,42 +103,42 @@ class NavigationSheetExtent extends SheetExtent { } @override - void dispatchUpdateNotification() { + void didUpdateMetrics() { // Do not dispatch a notifications if a local extent is active. if (activity is! NavigationSheetActivity) { - super.dispatchUpdateNotification(); + super.didUpdateMetrics(); } } @override - void dispatchDragStartNotification() { + void didDragStart(SheetDragStartDetails details) { // Do not dispatch a notifications if a local extent is active. if (activity is! NavigationSheetActivity) { - super.dispatchDragStartNotification(); + super.didDragStart(details); } } @override - void dispatchDragEndNotification() { + void didDragEnd(SheetDragEndDetails details) { // Do not dispatch a notifications if a local extent is active. if (activity is! NavigationSheetActivity) { - super.dispatchDragEndNotification(); + super.didDragEnd(details); } } @override - void dispatchDragUpdateNotification() { + void didDragUpdateMetrics(SheetDragUpdateDetails details) { // Do not dispatch a notifications if a local extent is active. if (activity is! NavigationSheetActivity) { - super.dispatchDragUpdateNotification(); + super.didDragUpdateMetrics(details); } } @override - void dispatchOverflowNotification(double overflow) { + void didOverflowBy(double overflow) { // Do not dispatch a notifications if a local extent is active. if (activity is! NavigationSheetActivity) { - super.dispatchOverflowNotification(overflow); + super.didOverflowBy(overflow); } } diff --git a/package/lib/src/scrollable/scrollable_sheet_activity.dart b/package/lib/src/scrollable/scrollable_sheet_activity.dart index cdd38b2d..17bc0e66 100644 --- a/package/lib/src/scrollable/scrollable_sheet_activity.dart +++ b/package/lib/src/scrollable/scrollable_sheet_activity.dart @@ -111,7 +111,6 @@ abstract class ScrollableSheetActivity final overflow = owner.physics.computeOverflow(delta, owner.metrics); if (overflow.abs() > 0) { position.didOverscrollBy(overflow); - owner.dispatchOverflowNotification(overflow); return overflow; } @@ -177,19 +176,29 @@ class DragScrollDrivenSheetActivity extends ScrollableSheetActivity } @override - void applyUserDragUpdate(Offset delta) { - scrollPosition.userScrollDirection = - delta.dy > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse; - _applyScrollOffset(-1 * delta.dy); + void applyUserDragUpdate(SheetDragUpdateDetails details) { + scrollPosition.userScrollDirection = details.deltaY > 0.0 + ? ScrollDirection.forward + : ScrollDirection.reverse; + final oldPixels = owner.metrics.pixels; + final overflow = _applyScrollOffset(-1 * details.deltaY); + if (owner.metrics.pixels != oldPixels) { + owner.didDragUpdateMetrics(details); + } + if (overflow > 0) { + owner.didOverflowBy(overflow); + } } @override - void applyUserDragEnd(Velocity velocity) { - owner.goBallisticWithScrollPosition( - velocity: -1 * velocity.pixelsPerSecond.dy, - shouldIgnorePointer: false, - scrollPosition: scrollPosition, - ); + void applyUserDragEnd(SheetDragEndDetails details) { + owner + ..didDragEnd(details) + ..goBallisticWithScrollPosition( + velocity: -1 * details.velocityY, + shouldIgnorePointer: false, + scrollPosition: scrollPosition, + ); } } @@ -230,10 +239,14 @@ class BallisticScrollDrivenSheetActivity extends ScrollableSheetActivity void onAnimationTick() { final delta = controller.value - _oldPixels; _oldPixels = controller.value; - final overscroll = _applyScrollOffset(delta); - - if (!overscroll.isApprox(0)) { - owner.goIdleWithScrollPosition(); + final overflow = _applyScrollOffset(delta); + if (owner.metrics.pixels != _oldPixels) { + owner.didUpdateMetrics(); + } + if (!overflow.isApprox(0)) { + owner + ..didOverflowBy(overflow) + ..goIdleWithScrollPosition(); return; } diff --git a/package/lib/src/scrollable/scrollable_sheet_extent.dart b/package/lib/src/scrollable/scrollable_sheet_extent.dart index 0768ffd9..966e6907 100644 --- a/package/lib/src/scrollable/scrollable_sheet_extent.dart +++ b/package/lib/src/scrollable/scrollable_sheet_extent.dart @@ -154,6 +154,7 @@ class ScrollableSheetExtent extends SheetExtent ), ); beginActivity(dragActivity); + didDragStart(startDetails); return drag; } diff --git a/package/test/foundation/sheet_notification_test.dart b/package/test/foundation/sheet_notification_test.dart new file mode 100644 index 00000000..aa1541a4 --- /dev/null +++ b/package/test/foundation/sheet_notification_test.dart @@ -0,0 +1,306 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:smooth_sheets/src/draggable/draggable_sheet.dart'; +import 'package:smooth_sheets/src/foundation/sheet_controller.dart'; +import 'package:smooth_sheets/src/foundation/sheet_extent.dart'; +import 'package:smooth_sheets/src/foundation/sheet_notification.dart'; +import 'package:smooth_sheets/src/foundation/sheet_physics.dart'; +import 'package:smooth_sheets/src/foundation/sheet_status.dart'; + +void main() { + testWidgets( + 'Drag gesture should dispatch drag start/update/end notifications in sequence', + (tester) async { + final reportedNotifications = []; + const targetKey = Key('target'); + + await tester.pumpWidget( + NotificationListener( + onNotification: (notification) { + reportedNotifications.add(notification); + return false; + }, + child: DraggableSheet( + minExtent: const Extent.pixels(0), + // Disable the snapping effect + physics: const ClampingSheetPhysics(), + child: Container( + key: targetKey, + color: Colors.white, + width: double.infinity, + height: 500, + ), + ), + ), + ); + + final gesturePointer = await tester.press(find.byKey(targetKey)); + await gesturePointer.moveBy(const Offset(0, 20)); + await tester.pump(); + expect(reportedNotifications, hasLength(2)); + expect( + reportedNotifications[0], + isA() + .having((e) => e.metrics.maybePixels, 'pixels', 500) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having((e) => e.dragDetails.kind, 'kind', PointerDeviceKind.touch) + .having( + (e) => e.dragDetails.localPosition, + 'localPosition', + const Offset(400, 250), + ) + .having( + (e) => e.dragDetails.globalPosition, + 'globalPosition', + const Offset(400, 350), + ), + ); + expect( + reportedNotifications[1], + isA() + .having((e) => e.metrics.maybePixels, 'pixels', 480) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having( + (e) => e.dragDetails.axisDirection, + 'axisDirection', + VerticalDirection.up, + ) + .having( + (e) => e.dragDetails.localPosition, + 'localPosition', + const Offset(400, 270), + ) + .having( + (e) => e.dragDetails.globalPosition, + 'globalPosition', + const Offset(400, 370), + ), + ); + + reportedNotifications.clear(); + await gesturePointer.moveBy(const Offset(0, 20)); + await tester.pump(); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.maybePixels, 'pixels', 460) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having( + (e) => e.dragDetails.axisDirection, + 'axisDirection', + VerticalDirection.up, + ) + .having( + (e) => e.dragDetails.localPosition, + 'localPosition', + const Offset(400, 290), + ) + .having( + (e) => e.dragDetails.globalPosition, + 'globalPosition', + const Offset(400, 390), + ), + ); + + reportedNotifications.clear(); + await gesturePointer.moveBy(const Offset(0, -20)); + await tester.pump(); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.maybePixels, 'pixels', 480) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having( + (e) => e.dragDetails.axisDirection, + 'axisDirection', + VerticalDirection.up, + ) + .having( + (e) => e.dragDetails.localPosition, + 'localPosition', + const Offset(400, 270), + ) + .having( + (e) => e.dragDetails.globalPosition, + 'globalPosition', + const Offset(400, 370), + ), + ); + + reportedNotifications.clear(); + await gesturePointer.up(); + await tester.pump(); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.maybePixels, 'pixels', 480) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having((e) => e.dragDetails.velocity, 'velocity', Velocity.zero) + .having( + (e) => e.dragDetails.axisDirection, + 'axisDirection', + VerticalDirection.up, + ), + ); + + reportedNotifications.clear(); + await tester.pumpAndSettle(); + expect(reportedNotifications, isEmpty, + reason: 'Once the drag is ended, ' + 'no notification should be dispatched.'); + }, + ); + + testWidgets( + 'Sheet animation should dispatch metrics update notifications', + (tester) async { + final reportedNotifications = []; + final controller = SheetController(); + + await tester.pumpWidget( + NotificationListener( + onNotification: (notification) { + reportedNotifications.add(notification); + return false; + }, + child: DraggableSheet( + controller: controller, + minExtent: const Extent.pixels(0), + // Disable the snapping effect + physics: const ClampingSheetPhysics(), + child: Container( + color: Colors.white, + width: double.infinity, + height: 600, + ), + ), + ), + ); + + unawaited( + controller.animateTo( + const Extent.pixels(0), + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), + ); + await tester.pump(Duration.zero); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.pixels, 'pixels', moreOrLessEquals(600)) + .having((e) => e.status, 'status', SheetStatus.animating), + ); + + reportedNotifications.clear(); + await tester.pump(const Duration(milliseconds: 100)); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.pixels, 'pixels', moreOrLessEquals(400)) + .having((e) => e.status, 'status', SheetStatus.animating), + ); + + reportedNotifications.clear(); + await tester.pump(const Duration(milliseconds: 100)); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.pixels, 'pixels', moreOrLessEquals(200)) + .having((e) => e.status, 'status', SheetStatus.animating), + ); + + reportedNotifications.clear(); + await tester.pump(const Duration(seconds: 100)); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.pixels, 'pixels', moreOrLessEquals(0)) + .having((e) => e.status, 'status', SheetStatus.animating), + ); + + reportedNotifications.clear(); + await tester.pumpAndSettle(); + expect(reportedNotifications, isEmpty, + reason: 'Once the animation is finished, ' + 'no notification should be dispatched.'); + }, + ); + + testWidgets( + 'Over-darg gesture should dispatch both darg and overflow notifications', + (tester) async { + final reportedNotifications = []; + const targetKey = Key('target'); + + await tester.pumpWidget( + NotificationListener( + onNotification: (notification) { + reportedNotifications.add(notification); + return false; + }, + child: DraggableSheet( + // Make sure the sheet can't be dragged + minExtent: const Extent.proportional(1), + maxExtent: const Extent.proportional(1), + // Disable the snapping effect + physics: const ClampingSheetPhysics(), + child: Container( + key: targetKey, + color: Colors.white, + width: double.infinity, + height: 500, + ), + ), + ), + ); + + final gesturePointer = await tester.press(find.byKey(targetKey)); + await gesturePointer.moveBy(const Offset(0, 20)); + await tester.pump(); + expect(reportedNotifications, hasLength(2)); + expect( + reportedNotifications[0], + isA().having( + (e) => e.dragDetails.axisDirection, + 'axisDirection', + // Since the y-axis is upward and we are performing a downward drag, + // the sign of the overflowed delta should be negative. + VerticalDirection.up, + ), + ); + expect( + reportedNotifications[1], + isA() + .having((e) => e.metrics.pixels, 'pixels', 500) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having((e) => e.overflow, 'overflow', -20), + ); + + reportedNotifications.clear(); + await gesturePointer.moveBy(const Offset(0, 20)); + await tester.pump(); + expect( + reportedNotifications.single, + isA() + .having((e) => e.metrics.pixels, 'pixels', 500) + .having((e) => e.status, 'status', SheetStatus.dragging) + .having((e) => e.overflow, 'overflow', -20), + ); + + reportedNotifications.clear(); + await gesturePointer.up(); + await tester.pump(); + expect(reportedNotifications.single, isA()); + + reportedNotifications.clear(); + await tester.pumpAndSettle(); + expect(reportedNotifications, isEmpty, + reason: 'Once the drag is ended, ' + 'no notification should be dispatched.'); + }, + ); +}