From dd7d162beb3869acf5bb78270fbd2d92fa3e3531 Mon Sep 17 00:00:00 2001 From: rumitvn <160252724+rumitvn@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:19:59 +0700 Subject: [PATCH] fix!: onDragCancel should not delegate to onDragEnd by default A cancellation is semantically different from a natural drag end: it has no velocity and means the gesture was interrupted (another recognizer won the arena, a scale takeover occurred, etc.). Delegating to onDragEnd made drag-to-dismiss style components apply their action on a cancel. With MultiDragScaleDispatcher every pinch cancels the individual pointer drags, so this is no longer the rare event the old comment claimed. onDragCancel now just resets isDragged; override it and call onDragEnd to keep the old behavior. Updated the docs and the tests that asserted delegation, and added a test isolating the new behavior. Closes #3926 --- doc/flame/inputs/drag_events.md | 7 +++- .../component_mixins/drag_callbacks.dart | 12 ++++-- .../component_mixins/drag_callbacks_test.dart | 39 +++++++++++++++++-- .../component_mixins/input_test_helper.dart | 11 +++++- .../scale_drag_callbacks_test.dart | 11 +++--- 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/doc/flame/inputs/drag_events.md b/doc/flame/inputs/drag_events.md index e0887366ac3..cb180b7a515 100644 --- a/doc/flame/inputs/drag_events.md +++ b/doc/flame/inputs/drag_events.md @@ -87,8 +87,11 @@ position associated with this event. ### onDragCancel -The precise semantics when this event occurs is not clear, so we provide a default implementation -which simply converts this event into an `onDragEnd`. +This event is fired when the drag gesture is interrupted before it ends naturally, for example when +another gesture recognizer wins the gesture arena or a second pointer triggers a scale takeover. +Unlike `onDragEnd` it carries no velocity information. The default implementation simply resets the +drag state; override it and call `onDragEnd` yourself if you want a cancellation handled identically +to a natural drag end. ## Mixins diff --git a/packages/flame/lib/src/events/component_mixins/drag_callbacks.dart b/packages/flame/lib/src/events/component_mixins/drag_callbacks.dart index bb05312471f..857c84e6e06 100644 --- a/packages/flame/lib/src/events/component_mixins/drag_callbacks.dart +++ b/packages/flame/lib/src/events/component_mixins/drag_callbacks.dart @@ -54,10 +54,16 @@ mixin DragCallbacks on Component { /// The drag was cancelled. /// - /// This is a very rare event, so we provide a default implementation that - /// converts it into an [onDragEnd] event. + /// Unlike [onDragEnd], a cancellation is not the natural end of a gesture: it + /// happens when the drag is interrupted (another recognizer wins the gesture + /// arena, a second pointer triggers a scale takeover, a system event, etc.), + /// so it carries no meaningful velocity. The default implementation only + /// resets the drag state. Override this and call [onDragEnd] yourself if you + /// want a cancellation handled identically to a natural drag end. @mustCallSuper - void onDragCancel(DragCancelEvent event) => onDragEnd(event.toDragEnd()); + void onDragCancel(DragCancelEvent event) { + _isDragged = false; + } @override @mustCallSuper diff --git a/packages/flame/test/events/component_mixins/drag_callbacks_test.dart b/packages/flame/test/events/component_mixins/drag_callbacks_test.dart index d52b1985605..4e78c0f90df 100644 --- a/packages/flame/test/events/component_mixins/drag_callbacks_test.dart +++ b/packages/flame/test/events/component_mixins/drag_callbacks_test.dart @@ -142,12 +142,41 @@ void main() { ); expect(component.dragCancelEvent, equals(1)); - expect(component.dragEndEvent, equals(1)); + // onDragCancel no longer delegates to onDragEnd. + expect(component.dragEndEvent, equals(0)); expect(component.isDragged, isFalse); dispatcher.onDragEnd(DragEndEvent(1, DragEndDetails())); expect(component.dragCancelEvent, equals(1)); - expect(component.dragEndEvent, equals(1)); + expect(component.dragEndEvent, equals(0)); + }, + ); + + testWithFlameGame( + 'onDragCancel resets isDragged without delegating to onDragEnd', + (game) async { + final component = DragCallbacksComponent() + ..x = 10 + ..y = 10 + ..width = 10 + ..height = 10; + await game.ensureAdd(component); + final dispatcher = game.firstChild()!; + + dispatcher.onDragStart( + createDragStartEvents( + game: game, + localPosition: const Offset(12, 12), + globalPosition: const Offset(12, 12), + ), + ); + expect(component.isDragged, isTrue); + + dispatcher.onDragCancel(DragCancelEvent(1)); + + expect(component.dragCancelEvent, equals(1)); + expect(component.dragEndEvent, equals(0)); + expect(component.isDragged, isFalse); }, ); @@ -271,6 +300,7 @@ void main() { var nDragStartCalled = 0; var nDragUpdateCalled = 0; var nDragEndCalled = 0; + var nDragCancelCalled = 0; final game = FlameGame( children: [ DragWithCallbacksComponent( @@ -279,6 +309,7 @@ void main() { onDragStart: (e) => nDragStartCalled++, onDragUpdate: (e) => nDragUpdateCalled++, onDragEnd: (e) => nDragEndCalled++, + onDragCancel: (e) => nDragCancelCalled++, ), ], ); @@ -306,7 +337,9 @@ void main() { await gesture.cancel(); await tester.pump(const Duration(seconds: 1)); expect(nDragStartCalled, 2); - expect(nDragEndCalled, 2); + expect(nDragCancelCalled, 1); + // The cancellation must not be reported as a drag end. + expect(nDragEndCalled, 1); }, ); diff --git a/packages/flame/test/events/component_mixins/input_test_helper.dart b/packages/flame/test/events/component_mixins/input_test_helper.dart index 79af9556f77..a940faa51dd 100644 --- a/packages/flame/test/events/component_mixins/input_test_helper.dart +++ b/packages/flame/test/events/component_mixins/input_test_helper.dart @@ -103,15 +103,18 @@ class DragWithCallbacksComponent extends PositionComponent with DragCallbacks { void Function(DragStartEvent)? onDragStart, void Function(DragUpdateEvent)? onDragUpdate, void Function(DragEndEvent)? onDragEnd, + void Function(DragCancelEvent)? onDragCancel, super.position, super.size, }) : _onDragStart = onDragStart, _onDragUpdate = onDragUpdate, - _onDragEnd = onDragEnd; + _onDragEnd = onDragEnd, + _onDragCancel = onDragCancel; final void Function(DragStartEvent)? _onDragStart; final void Function(DragUpdateEvent)? _onDragUpdate; final void Function(DragEndEvent)? _onDragEnd; + final void Function(DragCancelEvent)? _onDragCancel; @override void onDragStart(DragStartEvent event) { @@ -129,6 +132,12 @@ class DragWithCallbacksComponent extends PositionComponent with DragCallbacks { super.onDragEnd(event); return _onDragEnd?.call(event); } + + @override + void onDragCancel(DragCancelEvent event) { + super.onDragCancel(event); + return _onDragCancel?.call(event); + } } class ScaleWithCallbacksComponent extends PositionComponent diff --git a/packages/flame/test/events/component_mixins/scale_drag_callbacks_test.dart b/packages/flame/test/events/component_mixins/scale_drag_callbacks_test.dart index 0058860593e..9399a92d8d8 100644 --- a/packages/flame/test/events/component_mixins/scale_drag_callbacks_test.dart +++ b/packages/flame/test/events/component_mixins/scale_drag_callbacks_test.dart @@ -117,12 +117,13 @@ void main() { ); expect(component.dragCancelEvent, equals(1)); - expect(component.dragEndEvent, equals(1)); + // onDragCancel no longer delegates to onDragEnd. + expect(component.dragEndEvent, equals(0)); expect(component.isDragged, isFalse); dispatcher.onDragEnd(DragEndEvent(1, DragEndDetails())); expect(component.dragCancelEvent, equals(1)); - expect(component.dragEndEvent, equals(1)); + expect(component.dragEndEvent, equals(0)); }, ); @@ -188,13 +189,13 @@ void main() { dispatcher.onDragCancel(DragCancelEvent(1)); expect(component.dragCancelEvent, equals(1)); - // onDragCancel delegates to onDragEnd internally - expect(component.dragEndEvent, equals(1)); + // onDragCancel no longer delegates to onDragEnd. + expect(component.dragEndEvent, equals(0)); expect(component.isDragged, isFalse); // record removed after cancel; subsequent end for same pointer is a no-op dispatcher.onDragEnd(DragEndEvent(1, DragEndDetails())); - expect(component.dragEndEvent, equals(1)); + expect(component.dragEndEvent, equals(0)); }, );