Skip to content
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
7 changes: 5 additions & 2 deletions doc/flame/inputs/drag_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(event.toDragEnd())` yourself if you want a cancellation
handled identically to a natural drag end.


## Mixins
Expand Down
44 changes: 44 additions & 0 deletions doc/flame/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@ major versions of Flame, together with the steps required to migrate your code.
## Migrating from v1.38.0 to v2.0.0


### `onDragCancel` no longer delegates to `onDragEnd`

`DragCallbacks.onDragCancel` used to convert the cancellation into an `onDragEnd` event by default,
which made a cancelled drag look exactly like a completed one. A cancellation means that the gesture
was interrupted (another recognizer won the gesture arena, a second pointer triggered a scale
takeover, a system event, etc.) and it carries no velocity, so components such as drag-to-dismiss
would apply their action even though the drag never finished. This is not a rare event either, since
with `MultiDragScaleDispatcher` every two finger pinch cancels the individual pointer drags.

The default implementation now only resets `isDragged`, which means that `onDragEnd` is no longer
called when a drag is cancelled. If you were relying on the old behavior, override `onDragCancel` and
forward the event yourself with `DragCancelEvent.toDragEnd`:

```dart
// Before
class MyComponent extends PositionComponent with DragCallbacks {
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
// This also ran when the drag was cancelled.
dismiss();
}
}

// After
class MyComponent extends PositionComponent with DragCallbacks {
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
dismiss();
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
onDragEnd(event.toDragEnd());
}
}
```

If a cancelled drag should instead be reverted, put that logic in `onDragCancel` without calling
`onDragEnd`.


### `MultiDragDispatcher` removed

The deprecated `MultiDragDispatcher` and `MultiDragDispatcherKey` aliases have been removed. Use
Expand Down
8 changes: 8 additions & 0 deletions doc/tutorials/klondike/app/lib/step4/components/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,5 +262,13 @@ class Card extends PositionComponent with DragCallbacks {
}
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
// A cancelled drag is handled as if the card had been dropped where it is,
// otherwise the card would be left floating in the middle of the table.
onDragEnd(event.toDragEnd());
}

//#endregion
}
8 changes: 8 additions & 0 deletions doc/tutorials/klondike/app/lib/step5/components/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ class Card extends PositionComponent
}
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
// A cancelled drag is handled as if the card had been dropped where it is,
// otherwise the card would be left floating in the middle of the table.
onDragEnd(event.toDragEnd());
}

//#endregion

//#region Card-Tapping
Expand Down
14 changes: 14 additions & 0 deletions doc/tutorials/klondike/step4.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,20 @@ attached cards into the pile, and the same when it comes to returning the cards
}
```

There is one more case to take care of: a drag can be *cancelled* instead of ended, for example when
the player puts a second finger on the screen and the gesture turns into a pinch. Flame does not
turn a cancellation into an `onDragEnd` event, so unless we handle it the card would be left
floating in the middle of the table. Here we simply treat it as if the card had been dropped where
it currently is:

```dart
@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
onDragEnd(event.toDragEnd());
}
```

Well, this is it! The game is now fully playable. Press the button below to see what the resulting
code looks like, or to play it live. In the next section we will discuss how to make it more
animated with the help of effects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ class DraggableBall extends Ball with DragCallbacks {
super.onDragEnd(event);
paint = originalPaint;
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
paint = originalPaint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ class MouseJointWorld extends Forge2DWorld
@override
void onDragEnd(DragEndEvent info) {
super.onDragEnd(info);
_destroyMouseJoint();
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);
_destroyMouseJoint();
}

void _destroyMouseJoint() {
if (mouseJoint == null) {
return;
}
destroyJoint(mouseJoint!);
mouseJoint = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,15 @@ class DraggableBox extends Box with DragCallbacks {
_destroyJoint = true;
event.continuePropagation = false;
}

@override
void onDragCancel(DragCancelEvent event) {
super.onDragCancel(event);

if (mouseJoint == null) {
return;
}

_destroyJoint = true;
}
}
13 changes: 10 additions & 3 deletions packages/flame/lib/src/events/callbacks/drag_callbacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ 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(event.toDragEnd())` 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MultiDragScaleDispatcher>()!;

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);
},
);

Expand Down Expand Up @@ -271,6 +300,7 @@ void main() {
var nDragStartCalled = 0;
var nDragUpdateCalled = 0;
var nDragEndCalled = 0;
var nDragCancelCalled = 0;
final game = FlameGame(
children: [
DragWithCallbacksComponent(
Expand All @@ -279,6 +309,7 @@ void main() {
onDragStart: (e) => nDragStartCalled++,
onDragUpdate: (e) => nDragUpdateCalled++,
onDragEnd: (e) => nDragEndCalled++,
onDragCancel: (e) => nDragCancelCalled++,
),
],
);
Expand Down Expand Up @@ -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);
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ class DragWithCallbacksComponent extends PositionComponent with DragCallbacks {
this._onDragStart,
this._onDragUpdate,
this._onDragEnd,
this._onDragCancel,
super.position,
super.size,
});

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) {
Expand All @@ -120,6 +122,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
);

Expand Down Expand Up @@ -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));
},
);

Expand Down
Loading