Skip to content
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
87 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
b3c4b5b
Fix bugs, cleanup, and polish MultiDragScale recognizer PR
spydon Mar 2, 2026
c6a0b1c
Merge remote-tracking branch 'origin/main' into recognizer-v2
spydon Mar 2, 2026
b6e089e
test: Add dispatcher lifecycle and upgrade path tests
spydon Mar 2, 2026
7d260b3
feat: Add dynamic scale & drag example
spydon Mar 2, 2026
a77ceb0
docs: Document combining ScaleCallbacks and DragCallbacks
spydon Mar 2, 2026
9e23b3e
fix: Put control body on separate line in ScaleDispatcher
spydon Mar 2, 2026
82b4c3f
refactor: Remove unnecessary hide imports and run dart format
spydon Mar 2, 2026
77b94dc
Merge branch 'main' into recognizer-v2
spydon Mar 2, 2026
390a8cf
Update packages/flame/lib/src/events/multi_drag_scale_recognizer.dart
spydon Mar 3, 2026
03990fb
fix: Remove unused dragStartBehavior from MultiDragScaleGestureRecogn…
spydon Mar 3, 2026
3be0f7c
fix: Remove unused material.dart import from MultiDragDispatcher
spydon Mar 3, 2026
15a30aa
fix: Restore dt-based camera updates in scale_drag_example
spydon Mar 3, 2026
5904421
fix: Gate MultiDragDispatcher onStart callback when marked for removal
spydon Mar 3, 2026
2edf728
docs: Document markForRemoval gesture overlap behavior
spydon Mar 3, 2026
d980fb3
fix: Use scale focal point for drag updates during pinch gestures
spydon Mar 3, 2026
7968c39
fix: Compute focalPointDelta in global coordinates
spydon Mar 3, 2026
18276f0
chore: Rewrite timedZoomFrom test helper to remove CC BY-SA code
spydon Mar 3, 2026
31fa8ff
refactor: Remove abbreviations in timedZoomFrom test helper
spydon Mar 3, 2026
ad729ce
chore: Merge main into recognizer-v2
spydon May 17, 2026
acc0d12
fix: Remove markForRemoval in favor of immediate removal with cancel …
spydon May 17, 2026
6a16df8
fix: Restore markForRemoval - keep active gestures, reject new ones
spydon May 17, 2026
5f9cacb
fix: Re-initialize span baseline when second pointer is added
spydon May 17, 2026
cbfaf9e
test: Add rotation test for MultiDragScaleGestureRecognizer
spydon May 17, 2026
0d7017c
fix: Handle rejectGesture scale teardown and stale drag components
spydon May 17, 2026
47d51f2
fix: Each pointer fires its own drag update independently during scale
spydon May 17, 2026
fdf4b86
fix: Guard _dispose against double-resolving the arena entry
spydon May 17, 2026
f2a4163
Use Vector2.all everywhere in the example
spydon May 17, 2026
2e2623c
fix: Handle stale scale components in onScaleUpdate dispatchers
spydon May 17, 2026
a73da2e
fix: Prevent double translation in DragScaleBox example
spydon May 17, 2026
3a9a5e4
refactor: Extract dispatcher selection logic to shared helper
spydon May 17, 2026
9ebb9a2
refactor: Unify to single MultiDragScaleGestureRecognizer for all eve…
spydon May 17, 2026
7698025
fix: Update DynamicScaleDrag example to remove deleted dispatcher ref…
spydon May 17, 2026
e619fb9
fix: Remove dispatcher label from DynamicScaleDrag example
spydon May 17, 2026
b00f105
docs: Remove Dynamic addition sections from drag and scale event docs
spydon May 17, 2026
1498956
chore: Remove superseded scale_drag_example
spydon May 17, 2026
e442e1c
fix: Disable recognizer flags when last DragCallbacks/ScaleCallbacks …
spydon May 17, 2026
43defb4
refactor: Address review feedback on recognizer-v2 PR
spydon May 17, 2026
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
12 changes: 1 addition & 11 deletions doc/flame/inputs/scale_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Scale events** occur when the user moves two fingers in a pinch in, or in a pinch out move.
Only one single scale gesture can occur at the same time.


Comment thread
stilnat marked this conversation as resolved.
Outdated
For those components that you want to respond to scale events, add the `ScaleCallbacks` mixin.

- This mixin adds three overridable methods to your component: `onScaleStart`, `onScaleUpdate`,
Expand Down Expand Up @@ -171,14 +172,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 thread
spydon marked this conversation as resolved.
Outdated

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),
Comment thread
spydon marked this conversation as resolved.
Outdated
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();
}
Comment thread
spydon marked this conversation as resolved.
Outdated
}
}
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';
Comment thread
spydon marked this conversation as resolved.
Outdated

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
Comment thread
spydon marked this conversation as resolved.
Outdated
/// 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) {
Comment thread
spydon marked this conversation as resolved.
Outdated
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