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..05ae2de3 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]. @@ -373,6 +389,18 @@ class SheetDragController implements Drag, ScrollActivityDelegate { @mustCallSuper void dispose() { + // If the controller is disposed without calling end() or cancel(), + // we assume that the drag has been canceled. + if (_lastDetails is! SheetDragCancelDetails && + _lastDetails is! SheetDragEndDetails) { + final cancelDetails = SheetDragCancelDetails( + axisDirection: _target!.dragAxisDirection, + ); + _lastDetails = cancelDetails; + _gestureTamperer?.onDragCancel(cancelDetails); + _target!.onDragCancel(cancelDetails); + } + _target = null; _gestureTamperer = null; _impl.dispose(); 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/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