Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dispatch a notification when drag is canceled #204

Merged
merged 5 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 2 additions & 0 deletions package/lib/src/foundation/foundation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +27,7 @@ export 'sheet_extent.dart'
show Extent, FixedExtent, ProportionalExtent, SheetMetrics;
export 'sheet_notification.dart'
show
SheetDragCancelNotification,
SheetDragEndNotification,
SheetDragStartNotification,
SheetDragUpdateNotification,
Expand Down
7 changes: 7 additions & 0 deletions package/lib/src/foundation/sheet_activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ class DragSheetActivity extends SheetActivity
..didDragEnd(details)
..goBallistic(details.velocityY);
}

@override
void onDragCancel(SheetDragCancelDetails details) {
owner
..didDragCancel()
..goBallistic(0);
}
}

@internal
Expand Down
58 changes: 37 additions & 21 deletions package/lib/src/foundation/sheet_drag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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) {
Expand All @@ -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].
Expand Down
7 changes: 7 additions & 0 deletions package/lib/src/foundation/sheet_extent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions package/lib/src/foundation/sheet_gesture_tamperer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -54,6 +55,7 @@ class _TamperSheetGestureState extends State<TamperSheetGesture> {
}
}

// TODO: Rename to SheetGestureProxyScope.
class _TamperSheetGestureScope extends InheritedWidget {
const _TamperSheetGestureScope({
required this.tamperer,
Expand All @@ -68,6 +70,7 @@ class _TamperSheetGestureScope extends InheritedWidget {
}

// TODO: Expose this as a public API.
// TODO: Rename to SheetGestureProxyMixin.
@internal
mixin SheetGestureTamperer {
SheetGestureTamperer? _parent;
Expand All @@ -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,
Expand All @@ -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);
}
}
14 changes: 13 additions & 1 deletion package/lib/src/foundation/sheet_notification.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
41 changes: 30 additions & 11 deletions package/lib/src/modal/modal_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -372,8 +393,6 @@ class _SwipeDismissibleController with SheetGestureTamperer {
route.onPopInvoked(didPop);
}

return super.tamperWithDragEnd(
details.copyWith(velocityX: 0, velocityY: 0),
);
return true;
}
}
10 changes: 10 additions & 0 deletions package/lib/src/scrollable/scrollable_sheet_activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions package/test/foundation/sheet_notification_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <SheetNotification>[];
const targetKey = Key('target');

await tester.pumpWidget(
NotificationListener<SheetNotification>(
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<SheetDragStartNotification>(),
isA<SheetDragUpdateNotification>(),
]),
);

reportedNotifications.clear();
await gesturePointer.cancel();
await tester.pump();
expect(
reportedNotifications.single,
isA<SheetDragCancelNotification>(),
);

reportedNotifications.clear();
await tester.pumpAndSettle();
expect(reportedNotifications, isEmpty,
reason: 'Once the drag is canceled, '
'no notification should be dispatched.');
},
);
*/
}
Loading