Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b95ce23
add component based scaling gesture
stilnat Nov 13, 2025
4b8bc31
rename multi scale to scale
stilnat Nov 15, 2025
487e579
Delete my_tests_2 directory
stilnat Nov 15, 2025
01e81cd
Delete my_tests directory
stilnat Nov 15, 2025
8701f00
fix typo and format example
stilnat Nov 16, 2025
8b7105a
fix spelling
stilnat Nov 16, 2025
9723f97
remove TODO
stilnat Nov 16, 2025
28e24bf
format scale dispatcher
stilnat Nov 16, 2025
9c2acf6
remove automatically create multi drag dispatcher in Scale dispatcher
stilnat Nov 16, 2025
975bbdb
remove french
stilnat Nov 16, 2025
bea44fc
format scale dispatcher
stilnat Nov 16, 2025
d6f78cb
test first batch
stilnat Nov 19, 2025
987f977
add new test
stilnat Nov 21, 2025
f7b7153
format and change zoom api
stilnat Nov 21, 2025
cbc2333
fix name not recognised
stilnat Nov 21, 2025
6162fb3
format fix
stilnat Nov 21, 2025
528e665
Merge branch 'main' into scale-gesture
stilnat Nov 21, 2025
f945eea
fix melos analyse
stilnat Nov 21, 2025
8ddce28
format stuff
stilnat Nov 21, 2025
1ce9618
Merge remote-tracking branch 'upstream/main' into scale-gesture
stilnat Nov 21, 2025
c18fd0c
Merge branch 'main' into scale-gesture
spydon Nov 21, 2025
bf69b48
fix vector 2 creations
stilnat Nov 22, 2025
5369f79
add doc
stilnat Nov 22, 2025
c6e9b98
Merge branch 'main' into scale-gesture
stilnat Nov 22, 2025
0d10332
markdown fixes
stilnat Nov 22, 2025
4a8f8ad
Merge branch 'scale-gesture' of https://github.com/stilnat/flame into…
stilnat Nov 22, 2025
4441db2
fix markdown
stilnat Nov 22, 2025
47eb614
rename is scaled
stilnat Nov 22, 2025
64fe308
analyse fix
stilnat Nov 22, 2025
42f0d18
fix lint
stilnat Nov 22, 2025
d5b0086
add stuff about scale drag dispatcher
stilnat Nov 22, 2025
77df27a
add scale drag
stilnat Nov 23, 2025
bd6bd89
make gesture more than one sequence
stilnat Nov 23, 2025
d6995a8
add test
stilnat Nov 23, 2025
9a010d3
update recognizer
stilnat Nov 23, 2025
f1cd9eb
working recognizer pass all tests
stilnat Nov 24, 2025
b78a284
rewrite scale and drag callbacks without testing
stilnat Nov 24, 2025
0ff1070
make test pass without scaledrag callbacks
stilnat Nov 24, 2025
b21afe3
fix drag and scale callbacks to upgrade to scaleDragDispatcher
stilnat Nov 24, 2025
bf1398a
add a bunch of tests
stilnat Nov 24, 2025
58e986f
fix format and stuff
stilnat Nov 24, 2025
38d5e8c
make test include drag better
stilnat Nov 24, 2025
7e65ee6
factor test helper
stilnat Nov 24, 2025
2aa5be5
Merge remote-tracking branch 'upstream/main' into recognizer-v2
stilnat Nov 24, 2025
be53a17
rename scale example to scale drag
stilnat Nov 24, 2025
51b2308
remove french
stilnat Nov 25, 2025
31813e5
make consistent extension
stilnat Nov 25, 2025
5d58587
update doc
stilnat Nov 25, 2025
b34e0ca
fix small stuff
stilnat Nov 25, 2025
233d991
Update doc/flame/inputs/scale_events.md
stilnat Nov 27, 2025
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
11 changes: 0 additions & 11 deletions doc/flame/inputs/scale_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,3 @@ class ScaleOnlyRectangle extends RectangleComponent with ScaleCallbacks {
}

```


## Scale and drag gestures interactions

A multi drag gesture can sometimes look exactly like a scale gesture.
This is the case for instance, if you try to move two components toward each other at the same time.
If you added both a component using ScaleCallbacks and
one using DragCallbacks (or one using both), this issue will arise.
The Scale gesture will win over the drag gesture
and prevent your user to perform the multi drag gesture as they wanted. This is a limitation
with the current implementation that devs need to be aware of.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flame/components.dart' hide Matrix4;
import 'package:flame/events.dart' hide PointerMoveEvent;
import 'package:flame/game.dart' hide Matrix4;
import 'package:flutter/material.dart' hide PointerMoveEvent, Matrix4;
Comment on lines +2 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these hides really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be leftover I'll check


void main() {
runApp(GameWidget(game: ScaleExample()));
Expand All @@ -16,7 +16,7 @@ class ScaleExample extends FlameGame {
Vector2 zoomCenter = Vector2.zero();
double startingZoom = 1;

final bool addScaleOnlyRectangle = true;
final bool addScaleOnlyRectangle = false;
final bool addDragOnlyRectangle = true;
final bool addScaleDragRectangle = true;
final bool addZoom = false;
Expand Down Expand Up @@ -52,7 +52,7 @@ class ScaleExample extends FlameGame {

if (addScaleDragRectangle) {
interactiveRectangle = InteractiveRectangle(
position: Vector2(200, 200),
position: Vector2(100, 100),
size: Vector2.all(150),
color: Colors.red,
);
Expand All @@ -67,11 +67,11 @@ class ScaleExample extends FlameGame {
super.update(dt);

if (addCameraRotation) {
camera.viewfinder.angle += 0.1 * dt;
camera.viewfinder.angle += 0.001;
}
if (addZoom) {
debugText.text = '${camera.viewfinder.zoom}';
camera.viewfinder.zoom += 0.1 * dt;
camera.viewfinder.zoom += 0.001;
}
}
}
Expand Down Expand Up @@ -119,9 +119,6 @@ class InteractiveRectangle extends RectangleComponent
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
if (isScaling) {
return;
}
final rotated = event.canvasDelta.clone()
..rotate(game.camera.viewfinder.angle);
position.add(rotated);
Expand Down Expand Up @@ -210,7 +207,6 @@ class DragOnlyRectangle extends RectangleComponent
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
debugPrint('On Drag update');
final rotated = event.canvasDelta.clone()
..rotate(game.camera.viewfinder.angle);
position.add(rotated);
Expand Down
2 changes: 2 additions & 0 deletions packages/flame/lib/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export 'src/events/flame_game_mixins/multi_tap_dispatcher.dart'
show MultiTapDispatcher, MultiTapDispatcherKey;
export 'src/events/flame_game_mixins/pointer_move_dispatcher.dart'
show PointerMoveDispatcher, MouseMoveDispatcherKey;
export 'src/events/flame_game_mixins/scale_drag_dispatcher.dart'
show MultiDragScaleDispatcher, MultiDragScaleDispatcherKey;
export 'src/events/flame_game_mixins/secondary_tap_dispatcher.dart'
show SecondaryTapDispatcher, SecondaryTapDispatcherKey;
export 'src/events/game_mixins/multi_touch_drag_detector.dart'
Expand Down
47 changes: 44 additions & 3 deletions packages/flame/lib/src/events/component_mixins/drag_callbacks.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/src/events/flame_game_mixins/scale_dispatcher.dart';
import 'package:meta/meta.dart';

/// This mixin can be added to a [Component] allowing it to receive drag events.
Expand Down Expand Up @@ -61,11 +62,51 @@ mixin DragCallbacks on Component {
@mustCallSuper
void onMount() {
super.onMount();

final game = findRootGame()!;
if (game.findByKey(const MultiDragDispatcherKey()) == null) {
final dispatcher = MultiDragDispatcher();
game.registerKey(const MultiDragDispatcherKey(), dispatcher);
final scaleDispatcher = game.findByKey(const ScaleDispatcherKey());
final multiDragDispatcher = game.findByKey(const MultiDragDispatcherKey());
final multiDragScaleDispatcher = game.findByKey(
const MultiDragScaleDispatcherKey(),
);

// If MultiDragScaleDispatcher already exists, we're done
if (multiDragScaleDispatcher != null) {
return;
}

// If MultiDragDispatcher exists but component has ScaleCallbacks,
// upgrade it
if (multiDragDispatcher != null && this is ScaleCallbacks) {
final dispatcher = MultiDragScaleDispatcher();
game.registerKey(const MultiDragScaleDispatcherKey(), dispatcher);
game.add(dispatcher);
(multiDragDispatcher as MultiDragDispatcher).markForRemoval();
return;
}

// If MultiDragDispatcher exists and no ScaleCallbacks, we're done
if (multiDragDispatcher != null) {
return;
}

if (scaleDispatcher == null && multiDragDispatcher == null) {
// Check if component also has ScaleCallbacks
if (this is ScaleCallbacks) {
final dispatcher = MultiDragScaleDispatcher();
game.registerKey(const MultiDragScaleDispatcherKey(), dispatcher);
game.add(dispatcher);
} else {
final dispatcher = MultiDragDispatcher();
game.registerKey(const MultiDragDispatcherKey(), dispatcher);
game.add(dispatcher);
}
} else if (scaleDispatcher != null && multiDragDispatcher == null) {
// Upgrade ScaleDispatcher to MultiDragScaleDispatcher
final dispatcher = MultiDragScaleDispatcher();
game.registerKey(const MultiDragScaleDispatcherKey(), dispatcher);
game.add(dispatcher);
(scaleDispatcher as ScaleDispatcher).markForRemoval();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,32 @@ mixin ScaleCallbacks on Component {
@mustCallSuper
void onMount() {
super.onMount();
// Skip if DragCallbacks will handle it
if (this is DragCallbacks) {
return;
}

final game = findRootGame()!;
if (game.findByKey(const ScaleDispatcherKey()) == null) {
final scaleDispatcher = game.findByKey(const ScaleDispatcherKey());
final multiDragDispatcher = game.findByKey(const MultiDragDispatcherKey());
final multiDragScaleDispatcher = game.findByKey(
const MultiDragScaleDispatcherKey(),
);

// If MultiDragScaleDispatcher exists, DragCallbacks already handled it
if (multiDragScaleDispatcher != null) {
return;
}

if (scaleDispatcher == null && multiDragDispatcher == null) {
final dispatcher = ScaleDispatcher();
game.registerKey(const ScaleDispatcherKey(), dispatcher);
game.add(dispatcher);
} else if (scaleDispatcher == null && multiDragDispatcher != null) {
final dispatcher = MultiDragScaleDispatcher();
game.registerKey(const MultiDragScaleDispatcherKey(), dispatcher);
game.add(dispatcher);
(multiDragDispatcher as MultiDragDispatcher).markForRemoval();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/src/events/flame_drag_adapter.dart';
import 'package:flame/src/events/tagged_component.dart';
import 'package:flame/src/game/flame_game.dart';
import 'package:flame/src/game/game_render_box.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

class MultiDragDispatcherKey implements ComponentKey {
Expand All @@ -28,32 +27,10 @@ class MultiDragDispatcher extends Component implements MultiDragListener {
/// The record of all components currently being touched.
final Set<TaggedComponent<DragCallbacks>> _records = {};

final _dragUpdateController = StreamController<DragUpdateEvent>.broadcast(
sync: true,
);

Stream<DragUpdateEvent> get onUpdate => _dragUpdateController.stream;

final _dragStartController = StreamController<DragStartEvent>.broadcast(
sync: true,
);

Stream<DragStartEvent> get onStart => _dragStartController.stream;

final _dragEndController = StreamController<DragEndEvent>.broadcast(
sync: true,
);

Stream<DragEndEvent> get onEnd => _dragEndController.stream;

final _dragCancelController = StreamController<DragCancelEvent>.broadcast(
sync: true,
);

Stream<DragCancelEvent> get onCancel => _dragCancelController.stream;

FlameGame get game => parent! as FlameGame;

bool _shouldBeRemoved = false;

/// Called when the user initiates a drag gesture, for example by touching the
/// screen and then moving the finger.
///
Expand Down Expand Up @@ -134,39 +111,59 @@ class MultiDragDispatcher extends Component implements MultiDragListener {
@internal
@override
void handleDragStart(int pointerId, DragStartDetails details) {
if (_shouldBeRemoved) {
return;
}
final event = DragStartEvent(pointerId, game, details);
onDragStart(event);
_dragStartController.add(event);
}

@internal
@override
void handleDragUpdate(int pointerId, DragUpdateDetails details) {
final event = DragUpdateEvent(pointerId, game, details);
onDragUpdate(event);
_dragUpdateController.add(event);
}

@internal
@override
void handleDragEnd(int pointerId, DragEndDetails details) {
final event = DragEndEvent(pointerId, details);
onDragEnd(event);
_dragEndController.add(event);
_tryRemoving();
}

@internal
@override
void handleDragCancel(int pointerId) {
final event = DragCancelEvent(pointerId);
onDragCancel(event);
_dragCancelController.add(event);
_tryRemoving();
}

void markForRemoval() {
_shouldBeRemoved = true;
_tryRemoving();
}

bool _tryRemoving() {
// there's no more fingers
// that started dragging before _shouldBeRemoved flag was set to true.
if (_records.isEmpty && _shouldBeRemoved && isMounted) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it remove it directly even if there are fingers still left on the screen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the thought behind this is that removing immediately the recognizer would abruptly end currently performed gestures which sounds like unwanted behavior to me (but let me know if that's acceptable).

I didn't check if removing immediately would do that though but I wrote a test for that so I can try and run the test if I just call removeFromParent immediately instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should send out a cancel event for the gesture when this happens.
Imagine a game where you are steering the player using some drag inputs and then you want to add a cut scene or something, so you remove the input capabilities, then if the gestures aren't ended directly the user can still move the player until they release the gesture.

removeFromParent();
return true;
}
return false;
}

//#endregion

@override
void onMount() {
if (_tryRemoving()) {
return;
}

game.gestureDetectors.add<ImmediateMultiDragGestureRecognizer>(
ImmediateMultiDragGestureRecognizer.new,
(ImmediateMultiDragGestureRecognizer instance) {
Expand All @@ -179,10 +176,6 @@ class MultiDragDispatcher extends Component implements MultiDragListener {
void onRemove() {
game.gestureDetectors.remove<ImmediateMultiDragGestureRecognizer>();
game.unregisterKey(const MultiDragDispatcherKey());
_dragUpdateController.close();
_dragCancelController.close();
_dragStartController.close();
_dragEndController.close();
}

@override
Expand Down
Loading