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
5 changes: 5 additions & 0 deletions doc/flame/inputs/drag_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ local coordinate system.
Any component that receives `onDragStart` will later be receiving `onDragUpdate` and `onDragEnd`
events as well.

A drag only starts once the pointer has moved further than the platform's touch slop from the
point where it went down, so a tap with a slightly wobbling finger is still delivered as a tap and
not as a drag. When the drag starts, the movement accumulated before that point is delivered in the
first `onDragUpdate`.


### onDragUpdate

Expand Down
17 changes: 11 additions & 6 deletions packages/flame/lib/src/events/multi_drag_scale_recognizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -480,10 +480,17 @@ class _DragPointerState {
if (!_resolved) {
_pendingDelta += delta;
if (!recognizer.hasScale) {
// Drag-only mode: accept on any movement. If accepted synchronously,
// _accepted fires the initial update with _pendingDelta; no extra
// update fires here because we are still in the if(!_resolved) branch.
_arenaEntry?.resolve(GestureDisposition.accepted);
// Drag-only mode: accept once the accumulated movement exceeds the
// hit slop, matching ImmediateMultiDragGestureRecognizer. Some
// platforms emit zero-delta move events during a tap, so accepting on
// any move would steal the gesture from tap recognizers. If accepted
// synchronously, _accepted fires the initial update with
// _pendingDelta; no extra update fires here because we are still in
// the if(!_resolved) branch.
final hitSlop = computeHitSlop(kind, recognizer.gestureSettings);
if (_pendingDelta.distance > hitSlop) {
_arenaEntry?.resolve(GestureDisposition.accepted);
}
} else {
final distance = (currentPosition - initialPosition).distance;
if (distance > computePanSlop(kind, recognizer.gestureSettings)) {
Expand Down Expand Up @@ -511,12 +518,10 @@ class _DragPointerState {
if (_drag != null) {
_drag!.end(DragEndDetails(velocity: velocityTracker.getVelocity()));
}
_resolved = true;
}

void _cancel(PointerCancelEvent event) {
_drag?.cancel();
_resolved = true;
}

void _accepted(Drag? Function() starter) {
Expand Down
140 changes: 139 additions & 1 deletion packages/flame/test/events/component_mixins/drag_callbacks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ void main() {

// cancelled drag
final gesture = await tester.startGesture(const Offset(50, 50));
await gesture.moveBy(const Offset(10, 10));
await gesture.moveBy(const Offset(20, 20));
await gesture.cancel();
await tester.pump(const Duration(seconds: 1));
expect(nDragStartCalled, 2);
Expand All @@ -343,6 +343,127 @@ void main() {
},
);

testWidgets(
'tap is not cancelled when a DragCallbacks component is mounted first',
(tester) async {
var nDragStartCalled = 0;
final tapComponent = _TapCounterComponent(
position: Vector2(20, 20),
size: Vector2(100, 100),
);
final game = FlameGame(
children: [
DragWithCallbacksComponent(
position: Vector2(200, 200),
size: Vector2(100, 100),
onDragStart: (e) => nDragStartCalled++,
),
tapComponent,
],
);
await tester.pumpWidget(GameWidget(game: game));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));

await tester.tapAt(const Offset(50, 50));
await tester.pump(const Duration(seconds: 1));

expect(nDragStartCalled, 0);
expect(tapComponent.nTapDown, 1);
expect(tapComponent.nTapUp, 1);
expect(tapComponent.nTapCancel, 0);
},
);

testWidgets(
'zero-delta pointer moves do not start a drag or cancel a tap',
(tester) async {
var nDragStartCalled = 0;
final tapComponent = _TapCounterComponent(
position: Vector2(20, 20),
size: Vector2(100, 100),
);
final game = FlameGame(
children: [
DragWithCallbacksComponent(
position: Vector2(200, 200),
size: Vector2(100, 100),
onDragStart: (e) => nDragStartCalled++,
),
tapComponent,
],
);
await tester.pumpWidget(GameWidget(game: game));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));

final gesture = await tester.startGesture(const Offset(50, 50));
await gesture.moveBy(Offset.zero);
await gesture.moveBy(Offset.zero);
await gesture.moveBy(Offset.zero);
await gesture.up();
await tester.pump(const Duration(seconds: 1));

expect(nDragStartCalled, 0);
expect(tapComponent.nTapDown, 1);
expect(tapComponent.nTapUp, 1);
expect(tapComponent.nTapCancel, 0);
},
);

testWidgets(
'pointer moves below the touch slop do not start a drag',
(tester) async {
var nDragStartCalled = 0;
var nDragUpdateCalled = 0;
var nDragEndCalled = 0;
final tapComponent = _TapCounterComponent(
position: Vector2(20, 20),
size: Vector2(100, 100),
);
final game = FlameGame(
children: [
DragWithCallbacksComponent(
position: Vector2(20, 20),
size: Vector2(100, 100),
onDragStart: (e) => nDragStartCalled++,
onDragUpdate: (e) => nDragUpdateCalled++,
onDragEnd: (e) => nDragEndCalled++,
),
tapComponent,
],
);
await tester.pumpWidget(GameWidget(game: game));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));

// Movement below the touch slop is a tap, not a drag.
var gesture = await tester.startGesture(const Offset(50, 50));
await gesture.moveBy(const Offset(5, 5));
await gesture.moveBy(const Offset(5, 5));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(nDragStartCalled, 0);
expect(nDragUpdateCalled, 0);
expect(nDragEndCalled, 0);
expect(tapComponent.nTapUp, 1);
expect(tapComponent.nTapCancel, 0);

// Once the accumulated movement exceeds the touch slop the drag
// starts, and the pending movement is delivered as one update.
gesture = await tester.startGesture(const Offset(50, 50));
await gesture.moveBy(const Offset(5, 5));
await gesture.moveBy(const Offset(20, 20));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(nDragStartCalled, 1);
expect(nDragUpdateCalled, 1);
expect(nDragEndCalled, 1);
expect(tapComponent.nTapUp, 1);
expect(tapComponent.nTapCancel, 1);
},
);

testWidgets(
'drag event does not affect more than one component',
(tester) async {
Expand Down Expand Up @@ -506,3 +627,20 @@ void main() {
},
);
}

class _TapCounterComponent extends PositionComponent with TapCallbacks {
_TapCounterComponent({super.position, super.size});

int nTapDown = 0;
int nTapUp = 0;
int nTapCancel = 0;

@override
void onTapDown(TapDownEvent event) => nTapDown++;

@override
void onTapUp(TapUpEvent event) => nTapUp++;

@override
void onTapCancel(TapCancelEvent event) => nTapCancel++;
}
Loading