diff --git a/package/CHANGELOG.md b/package/CHANGELOG.md index 248ce0b0..6ec9d7e0 100644 --- a/package/CHANGELOG.md +++ b/package/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.0 MM DD, 2024 + +- Dispatch a notification when drag is cancelled (#204) + ## 0.8.2 Jul 11, 2024 - Fix: Opening keyboard interrupts sheet animation (#189) diff --git a/package/lib/src/foundation/foundation.dart b/package/lib/src/foundation/foundation.dart index e786d16b..858e0bef 100644 --- a/package/lib/src/foundation/foundation.dart +++ b/package/lib/src/foundation/foundation.dart @@ -18,6 +18,7 @@ export 'sheet_content_scaffold.dart' export 'sheet_controller.dart' show DefaultSheetController, SheetController; export 'sheet_drag.dart' show + SheetDragCancelDetails, SheetDragDetails, SheetDragEndDetails, SheetDragStartDetails, @@ -26,6 +27,7 @@ export 'sheet_extent.dart' show Extent, FixedExtent, ProportionalExtent, SheetMetrics; export 'sheet_notification.dart' show + SheetDragCancelNotification, SheetDragEndNotification, SheetDragStartNotification, SheetDragUpdateNotification, diff --git a/package/lib/src/foundation/sheet_activity.dart b/package/lib/src/foundation/sheet_activity.dart index 21322980..0428b6b0 100644 --- a/package/lib/src/foundation/sheet_activity.dart +++ b/package/lib/src/foundation/sheet_activity.dart @@ -281,6 +281,13 @@ class DragSheetActivity extends SheetActivity ..didDragEnd(details) ..goBallistic(details.velocityY); } + + @override + void onDragCancel(SheetDragCancelDetails details) { + owner + ..didDragCancel() + ..goBallistic(0); + } } @internal diff --git a/package/lib/src/foundation/sheet_drag.dart b/package/lib/src/foundation/sheet_drag.dart index 103af025..e5470ce3 100644 --- a/package/lib/src/foundation/sheet_drag.dart +++ b/package/lib/src/foundation/sheet_drag.dart @@ -214,12 +214,22 @@ class SheetDragEndDetails extends SheetDragDetails { } } +/// Details for when a sheet drag is canceled. +class SheetDragCancelDetails extends SheetDragDetails { + /// Creates details for when a sheet drag is canceled. + SheetDragCancelDetails({required super.axisDirection}); +} + @internal abstract class SheetDragControllerTarget { VerticalDirection get dragAxisDirection; + // TODO: Rename to onDragUpdate. void applyUserDragUpdate(SheetDragUpdateDetails details); + // TODO: Rename to onDragEnd. void applyUserDragEnd(SheetDragEndDetails details); + void onDragCancel(SheetDragCancelDetails details); + /// Returns the minimum number of pixels that the sheet being dragged /// will potentially consume for the given drag delta. /// @@ -265,12 +275,18 @@ class SheetDragController implements Drag, ScrollActivityDelegate { /// to avoid duplicating the code of [ScrollDragController]. late final ScrollDragController _impl; + // TODO: Remove unnecessary nullability. SheetDragControllerTarget? _target; + + // TODO: Rename to _gestureProxy. SheetGestureTamperer? _gestureTamperer; - SheetDragDetails _lastDetails; + /// The details of the most recently observed drag event. SheetDragDetails get lastDetails => _lastDetails; + SheetDragDetails _lastDetails; + /// The most recently observed [DragStartDetails], [DragUpdateDetails], or + /// [DragEndDetails] object. dynamic get lastRawDetails => _impl.lastDetails; void updateTarget(SheetDragControllerTarget delegate) { @@ -296,29 +312,29 @@ class SheetDragController implements Drag, ScrollActivityDelegate { _impl.cancel(); } - /// Called by the [ScrollDragController] in [Drag.end] and [Drag.cancel]. + /// Called by the [ScrollDragController] in either [ScrollDragController.end] + /// or [ScrollDragController.cancel]. @override void goBallistic(double velocity) { - var details = switch (_impl.lastDetails) { - final DragEndDetails details => SheetDragEndDetails( - axisDirection: _target!.dragAxisDirection, - velocityX: details.velocity.pixelsPerSecond.dx, - velocityY: -1 * velocity, - ), - // Drag was canceled. - _ => SheetDragEndDetails( - axisDirection: _target!.dragAxisDirection, - velocityX: 0, - velocityY: 0, - ), - }; - - if (_gestureTamperer case final tamper?) { - details = tamper.tamperWithDragEnd(details); + if (_impl.lastDetails case final DragEndDetails rawDetails) { + var endDetails = SheetDragEndDetails( + axisDirection: _target!.dragAxisDirection, + velocityX: rawDetails.velocity.pixelsPerSecond.dx, + velocityY: -1 * velocity, + ); + if (_gestureTamperer case final tamper?) { + endDetails = tamper.tamperWithDragEnd(endDetails); + } + _lastDetails = endDetails; + _target!.applyUserDragEnd(endDetails); + } else { + final cancelDetails = SheetDragCancelDetails( + axisDirection: _target!.dragAxisDirection, + ); + _lastDetails = cancelDetails; + _gestureTamperer?.onDragCancel(cancelDetails); + _target!.onDragCancel(cancelDetails); } - - _lastDetails = details; - _target!.applyUserDragEnd(details); } /// Called by the [ScrollDragController] in [Drag.update]. diff --git a/package/lib/src/foundation/sheet_extent.dart b/package/lib/src/foundation/sheet_extent.dart index 82a9048d..436e7487 100644 --- a/package/lib/src/foundation/sheet_extent.dart +++ b/package/lib/src/foundation/sheet_extent.dart @@ -542,6 +542,13 @@ abstract class SheetExtent extends ChangeNotifier ).dispatch(context.notificationContext); } + void didDragCancel() { + assert(metrics.hasDimensions); + SheetDragCancelNotification( + metrics: metrics, + ).dispatch(context.notificationContext); + } + void didOverflowBy(double overflow) { assert(metrics.hasDimensions); SheetOverflowNotification( diff --git a/package/lib/src/foundation/sheet_gesture_tamperer.dart b/package/lib/src/foundation/sheet_gesture_tamperer.dart index f46dc743..1a847dc2 100644 --- a/package/lib/src/foundation/sheet_gesture_tamperer.dart +++ b/package/lib/src/foundation/sheet_gesture_tamperer.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import 'sheet_drag.dart'; // TODO: Expose this as a public API. +// TODO: Rename to SheetGestureProxy. @internal class TamperSheetGesture extends StatefulWidget { const TamperSheetGesture({ @@ -54,6 +55,7 @@ class _TamperSheetGestureState extends State { } } +// TODO: Rename to SheetGestureProxyScope. class _TamperSheetGestureScope extends InheritedWidget { const _TamperSheetGestureScope({ required this.tamperer, @@ -68,6 +70,7 @@ class _TamperSheetGestureScope extends InheritedWidget { } // TODO: Expose this as a public API. +// TODO: Rename to SheetGestureProxyMixin. @internal mixin SheetGestureTamperer { SheetGestureTamperer? _parent; @@ -79,12 +82,14 @@ mixin SheetGestureTamperer { @useResult @mustCallSuper + // TODO: Rename to onDragStart. SheetDragStartDetails tamperWithDragStart(SheetDragStartDetails details) { return _parent?.tamperWithDragStart(details) ?? details; } @useResult @mustCallSuper + // TODO: Rename to onDragUpdate. SheetDragUpdateDetails tamperWithDragUpdate( SheetDragUpdateDetails details, Offset minPotentialDeltaConsumption, @@ -100,7 +105,13 @@ mixin SheetGestureTamperer { @useResult @mustCallSuper + // TODO: Rename to onDragEnd. SheetDragEndDetails tamperWithDragEnd(SheetDragEndDetails details) { return _parent?.tamperWithDragEnd(details) ?? details; } + + @mustCallSuper + void onDragCancel(SheetDragCancelDetails details) { + _parent?.onDragCancel(details); + } } diff --git a/package/lib/src/foundation/sheet_notification.dart b/package/lib/src/foundation/sheet_notification.dart index 1f426805..301a7a10 100644 --- a/package/lib/src/foundation/sheet_notification.dart +++ b/package/lib/src/foundation/sheet_notification.dart @@ -8,7 +8,7 @@ import 'sheet_status.dart'; /// A [Notification] that is dispatched when the sheet extent changes. /// /// Sheet widgets notify their ancestors about changes to their extent. -/// There are 5 types of notifications: +/// There are 6 types of notifications: /// - [SheetOverflowNotification], which is dispatched when the user tries /// to drag the sheet beyond its draggable bounds but the sheet has not /// changed its extent because its [SheetPhysics] does not allow it to be. @@ -20,6 +20,8 @@ import 'sheet_status.dart'; /// dragging the sheet. /// - [SheetDragEndNotification], which is dispatched when the user stops /// dragging the sheet. +/// - [SheetDragCancelNotification], which is dispatched when the user +/// or the system cancels a drag gesture in the sheet. /// /// See also: /// - [NotificationListener], which can be used to listen for notifications @@ -116,6 +118,16 @@ class SheetDragEndNotification extends SheetNotification { } } +/// A [SheetNotification] that is dispatched when the user +/// or the system cancels a drag gesture in the sheet. +class SheetDragCancelNotification extends SheetNotification { + /// Create a notification that is dispatched when a drag gesture + /// in the sheet is canceled. + const SheetDragCancelNotification({ + required super.metrics, + }) : super(status: SheetStatus.dragging); +} + /// A [SheetNotification] that is dispatched when the user tries /// to drag the sheet beyond its draggable bounds but the sheet has not /// changed its extent because its [SheetPhysics] does not allow it to be. diff --git a/package/lib/src/modal/modal_sheet.dart b/package/lib/src/modal/modal_sheet.dart index 7f5e9ddf..8d4660d5 100644 --- a/package/lib/src/modal/modal_sheet.dart +++ b/package/lib/src/modal/modal_sheet.dart @@ -302,28 +302,49 @@ class _SwipeDismissibleController with SheetGestureTamperer { @override SheetDragEndDetails tamperWithDragEnd(SheetDragEndDetails details) { + final wasHandled = _handleDragEnd( + velocity: details.velocity, + axisDirection: details.axisDirection, + ); + return wasHandled + ? super.tamperWithDragEnd(details.copyWith(velocityX: 0, velocityY: 0)) + : super.tamperWithDragEnd(details); + } + + @override + void onDragCancel(SheetDragCancelDetails details) { + super.onDragCancel(details); + _handleDragEnd( + axisDirection: details.axisDirection, + velocity: Velocity.zero, + ); + } + + bool _handleDragEnd({ + required Velocity velocity, + required VerticalDirection axisDirection, + }) { if (!_isUserGestureInProgress || transitionController.isAnimating) { - return super.tamperWithDragEnd(details); + return false; } final viewportHeight = _context.size!.height; final draggedDistance = viewportHeight * (1 - transitionController.value); - final velocity = switch (details.axisDirection) { - VerticalDirection.up => - details.velocity.pixelsPerSecond.dy / viewportHeight, + final effectiveVelocity = switch (axisDirection) { + VerticalDirection.up => velocity.pixelsPerSecond.dy / viewportHeight, VerticalDirection.down => - -1 * details.velocity.pixelsPerSecond.dy / viewportHeight, + -1 * velocity.pixelsPerSecond.dy / viewportHeight, }; final bool invokePop; if (MediaQuery.viewInsetsOf(_context).bottom > 0) { // The on-screen keyboard is open. invokePop = false; - } else if (velocity < 0) { + } else if (effectiveVelocity < 0) { // Flings down. - invokePop = velocity.abs() > _minFlingVelocityToDismiss; - } else if (velocity.isApprox(0)) { + invokePop = effectiveVelocity.abs() > _minFlingVelocityToDismiss; + } else if (effectiveVelocity.isApprox(0)) { assert(draggedDistance >= 0); // Dragged down enough to dismiss. invokePop = draggedDistance > _minDragDistanceToDismiss; @@ -372,8 +393,6 @@ class _SwipeDismissibleController with SheetGestureTamperer { route.onPopInvoked(didPop); } - return super.tamperWithDragEnd( - details.copyWith(velocityX: 0, velocityY: 0), - ); + return true; } } diff --git a/package/lib/src/scrollable/scrollable_sheet_activity.dart b/package/lib/src/scrollable/scrollable_sheet_activity.dart index ab4c7a7e..c7578116 100644 --- a/package/lib/src/scrollable/scrollable_sheet_activity.dart +++ b/package/lib/src/scrollable/scrollable_sheet_activity.dart @@ -211,6 +211,16 @@ class DragScrollDrivenSheetActivity extends ScrollableSheetActivity scrollPosition: scrollPosition, ); } + + @override + void onDragCancel(SheetDragCancelDetails details) { + owner + ..didDragCancel() + ..goBallisticWithScrollPosition( + velocity: 0, + scrollPosition: scrollPosition, + ); + } } /// A [SheetActivity] that animates either a scrollable content of diff --git a/package/test/foundation/sheet_notification_test.dart b/package/test/foundation/sheet_notification_test.dart index aa1541a4..8fe7d21b 100644 --- a/package/test/foundation/sheet_notification_test.dart +++ b/package/test/foundation/sheet_notification_test.dart @@ -303,4 +303,60 @@ void main() { 'no notification should be dispatched.'); }, ); + + /* + TODO: Uncomment this once https://github.com/flutter/flutter/issues/152163 is fixed. + testWidgets( + 'Canceling drag gesture should dispatch a drag cancel notification', + (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, + equals([ + isA(), + isA(), + ]), + ); + + reportedNotifications.clear(); + await gesturePointer.cancel(); + await tester.pump(); + expect( + reportedNotifications.single, + isA(), + ); + + reportedNotifications.clear(); + await tester.pumpAndSettle(); + expect(reportedNotifications, isEmpty, + reason: 'Once the drag is canceled, ' + 'no notification should be dispatched.'); + }, + ); + */ }