Drag events¶
Note
This document describes a new experimental API. The more traditional approach for handling drag events is described in Gesture Input.
Drag events occur when the user moves their finger across the screen of the device, or when they move the mouse while holding its button down.
Multiple drag events can occur at the same time, if the user is using multiple fingers. Such cases
will be handled correctly by Flame, and you can even keep track of the events by using their
pointerId
property.
In order to enable drag events for your game, do the following:
Add the
HasDraggableComponents
mixin to your main game class:class MyGame extends FlameGame with HasDraggableComponents { // ... }
For those components that you want to respond to drags, add the
DragCallbacks
mixin.This mixin adds four overridable methods to your component:
onDragStart
,onDragUpdate
,onDragEnd
, andonDragCancel
. By default, these methods do nothing – they need to be overridden in order to perform any function.In addition, the component must implement the
containsLocalPoint()
method – this method allows Flame to know whether the event occurred within the component or not.
class MyComponent extends PositionComponent with DragCallbacks { MyComponent() : super(size: Vector2(180, 120)); @override void onDragStart(DragStartEvent event) { // Do something in response to a drag event } }
Demo¶
In this example you can use drag gestures to either drag star-like shapes across the screen, or to draw curves inside the pink rectangle.
1import 'dart:math';
2
3import 'package:flame/components.dart';
4import 'package:flame/experimental.dart';
5import 'package:flame/game.dart';
6import 'package:flutter/rendering.dart';
7
8/// The main [FlameGame] class uses [HasDraggableComponents] in order to enable
9/// tap events propagation.
10class DragEventsGame extends FlameGame with HasDraggableComponents {
11 @override
12 Future<void> onLoad() async {
13 addAll([
14 DragTarget(),
15 Star(
16 n: 5,
17 radius1: 40,
18 radius2: 20,
19 sharpness: 0.2,
20 color: const Color(0xffbae5ad),
21 position: Vector2(70, 70),
22 ),
23 Star(
24 n: 3,
25 radius1: 50,
26 radius2: 40,
27 sharpness: 0.3,
28 color: const Color(0xff6ecbe5),
29 position: Vector2(70, 160),
30 ),
31 Star(
32 n: 12,
33 radius1: 10,
34 radius2: 75,
35 sharpness: 1.3,
36 color: const Color(0xfff6df6a),
37 position: Vector2(70, 270),
38 ),
39 Star(
40 n: 10,
41 radius1: 20,
42 radius2: 17,
43 sharpness: 0.85,
44 color: const Color(0xfff82a4b),
45 position: Vector2(110, 110),
46 ),
47 ]);
48 }
49}
50
51/// This component is the pink-ish rectangle in the center of the game window.
52/// It uses the [DragCallbacks] mixin in order to inform the game that it wants
53/// to receive drag events.
54class DragTarget extends PositionComponent with DragCallbacks {
55 DragTarget() : super(anchor: Anchor.center);
56
57 final _rectPaint = Paint()..color = const Color(0x88AC54BF);
58
59 /// We will store all current circles into this map, keyed by the `pointerId`
60 /// of the event that created the circle.
61 final Map<int, Trail> _trails = {};
62
63 @override
64 void onGameResize(Vector2 canvasSize) {
65 super.onGameResize(canvasSize);
66 size = canvasSize - Vector2(100, 75);
67 if (size.x < 100 || size.y < 100) {
68 size = canvasSize * 0.9;
69 }
70 position = canvasSize / 2;
71 }
72
73 @override
74 void render(Canvas canvas) {
75 canvas.drawRect(size.toRect(), _rectPaint);
76 }
77
78 @override
79 void onDragStart(DragStartEvent event) {
80 final trail = Trail(event.localPosition);
81 _trails[event.pointerId] = trail;
82 add(trail);
83 }
84
85 @override
86 void onDragUpdate(DragUpdateEvent event) {
87 _trails[event.pointerId]!.addPoint(event.localPosition);
88 }
89
90 @override
91 void onDragEnd(DragEndEvent event) {
92 _trails.remove(event.pointerId)!.end();
93 }
94
95 @override
96 void onDragCancel(DragCancelEvent event) {
97 _trails.remove(event.pointerId)!.cancel();
98 }
99}
100
101class Trail extends Component {
102 Trail(Vector2 origin)
103 : _paths = [Path()..moveTo(origin.x, origin.y)],
104 _opacities = [1],
105 _lastPoint = origin.clone(),
106 _color =
107 HSLColor.fromAHSL(1, random.nextDouble() * 360, 1, 0.8).toColor();
108
109 final List<Path> _paths;
110 final List<double> _opacities;
111 Color _color;
112 late final _linePaint = Paint()..style = PaintingStyle.stroke;
113 late final _circlePaint = Paint()..color = _color;
114 bool _released = false;
115 double _timer = 0;
116 final _vanishInterval = 0.03;
117 final Vector2 _lastPoint;
118
119 static final random = Random();
120 static const lineWidth = 10.0;
121
122 @override
123 void render(Canvas canvas) {
124 assert(_paths.length == _opacities.length);
125 for (var i = 0; i < _paths.length; i++) {
126 final path = _paths[i];
127 final opacity = _opacities[i];
128 if (opacity > 0) {
129 _linePaint.color = _color.withOpacity(opacity);
130 _linePaint.strokeWidth = lineWidth * opacity;
131 canvas.drawPath(path, _linePaint);
132 }
133 }
134 canvas.drawCircle(
135 _lastPoint.toOffset(),
136 (lineWidth - 2) * _opacities.last + 2,
137 _circlePaint,
138 );
139 }
140
141 @override
142 void update(double dt) {
143 assert(_paths.length == _opacities.length);
144 _timer += dt;
145 while (_timer > _vanishInterval) {
146 _timer -= _vanishInterval;
147 for (var i = 0; i < _paths.length; i++) {
148 _opacities[i] -= 0.01;
149 if (_opacities[i] <= 0) {
150 _paths[i].reset();
151 }
152 }
153 if (!_released) {
154 _paths.add(Path()..moveTo(_lastPoint.x, _lastPoint.y));
155 _opacities.add(1);
156 }
157 }
158 if (_opacities.last < 0) {
159 removeFromParent();
160 }
161 }
162
163 void addPoint(Vector2 point) {
164 if (!point.x.isNaN) {
165 for (final path in _paths) {
166 path.lineTo(point.x, point.y);
167 }
168 _lastPoint.setFrom(point);
169 }
170 }
171
172 void end() => _released = true;
173
174 void cancel() {
175 _released = true;
176 _color = const Color(0xFFFFFFFF);
177 }
178}
179
180class Star extends PositionComponent with DragCallbacks {
181 Star({
182 required int n,
183 required double radius1,
184 required double radius2,
185 required double sharpness,
186 required this.color,
187 super.position,
188 }) {
189 _path = Path()..moveTo(radius1, 0);
190 for (var i = 0; i < n; i++) {
191 final p1 = Vector2(radius2, 0)..rotate(tau / n * (i + sharpness));
192 final p2 = Vector2(radius2, 0)..rotate(tau / n * (i + 1 - sharpness));
193 final p3 = Vector2(radius1, 0)..rotate(tau / n * (i + 1));
194 _path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
195 }
196 _path.close();
197 }
198
199 final Color color;
200 final Paint _paint = Paint();
201 final Paint _borderPaint = Paint()
202 ..color = const Color(0xFFffffff)
203 ..style = PaintingStyle.stroke
204 ..strokeWidth = 3;
205 final _shadowPaint = Paint()
206 ..color = const Color(0xFF000000)
207 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
208 late final Path _path;
209 bool _isDragged = false;
210
211 @override
212 bool containsLocalPoint(Vector2 point) {
213 return _path.contains(point.toOffset());
214 }
215
216 @override
217 void render(Canvas canvas) {
218 if (_isDragged) {
219 _paint.color = color.withOpacity(0.5);
220 canvas.drawPath(_path, _paint);
221 canvas.drawPath(_path, _borderPaint);
222 } else {
223 _paint.color = color.withOpacity(1);
224 canvas.drawPath(_path, _shadowPaint);
225 canvas.drawPath(_path, _paint);
226 }
227 }
228
229 @override
230 void onDragStart(DragStartEvent event) {
231 _isDragged = true;
232 priority = 10;
233 }
234
235 @override
236 void onDragEnd(DragEndEvent event) {
237 _isDragged = false;
238 priority = 0;
239 }
240
241 @override
242 void onDragUpdate(DragUpdateEvent event) {
243 position += event.delta;
244 }
245}
246
247const tau = 2 * pi;
Drag anatomy¶
onDragStart¶
This is the first event that occurs in a drag sequence. Usually, the event will be delivered to the
topmost component at the point of touch with the DragCallbacks
mixin. However, by setting the flag
event.continuePropagation
to true, you can allow the event to propagate to the components below.
The DragStartEvent
object associated with this event will contain the coordinate of the point
where the event has originated. This point is available in multiple coordinate system:
devicePosition
is given in the coordinate system of the entire device, canvasPosition
is in the
coordinate system of the game widget, and localPosition
provides the position in the component’s
local coordinate system.
Any component that receives onDragStart
will later be receiving onDragUpdate
and onDragEnd
events as well.
onDragUpdate¶
This event is fired continuously as user drags their finger across the screen. It will not fire if the user is holding their finger still.
The default implementation delivers this event to all the components that received the previous
onDragStart
with the same pointer id. If the point of touch is still within the component, then
event.localPosition
will give the position of that point in the local coordinate system. However,
if the user moves their finger away from the component, the property event.localPosition
will
return a point whose coordinates are NaNs. Likewise, the event.renderingTrace
in this case will be
empty. However, the canvasPosition
and devicePosition
properties of the event will be valid.
In addition, the DragUpdateEvent
will contain delta
– the amount the finger has moved since the
previous onDragUpdate
, or since the onDragStart
if this is the first drag-update after a drag-
start.
The event.timestamp
property measures the time elapsed since the beginning of the drag. It can be
used, for example, to compute the speed of the movement.
onDragEnd¶
This event is fired when the user lifts their finger and thus stops the drag gesture. There is no 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
.
Mixins¶
HasDraggableComponents¶
This mixin is used on a FlameGame
in order to ensure that drag events coming from Flutter reach
their target Component
s. This mixin must be added if you have any components with the
DragCallbacks
mixin.
The mixin adds methods onDragStart
, onDragUpdate
, onDragEnd
, and onDragCancel
to the game.
The default implementation will simply propagate these events to the component(s) that are at the
point of touch; but you can override them if you also want to respond to those events at the global
game level:
class MyGame extends FlameGame with HasDraggableComponents {
@override
void onDragDown(DragDownEvent event) {
super.onDragDown(event);
if (!event.handled) {
print('Event $event was not handled by any component');
}
}
}
DragCallbacks¶
The DragCallbacks
mixin can be added to any Component
in order for that component to start
receiving drag events.
This mixin adds methods onDragStart
, onDragUpdate
, onDragEnd
, and onDragCancel
to the
component, which by default don’t do anything, but can be overridden to implement any real
functionality.
Another crucial detail is that a component will only receive drag events that originate within
that component, as judged by the containsLocalPoint()
function. The commonly-used
PositionComponent
class provides such an implementation based on its size
property. Thus, if
your component derives from a PositionComponent
, then make sure that you set its size correctly.
If, however, your component derives from the bare Component
, then the containsLocalPoint()
method must be implemented manually.
If your component is a part of a larger hierarchy, then it will only receive drag events if its
ancestors have all implemented the containsLocalPoint
correctly.
class MyComponent extends PositionComponent with DragCallbacks {
MyComponent({super.size});
final _paint = Paint();
bool _isDragged = false;
@override
void onDragStart(DragStartEvent event) => _isDragged = true;
@override
void onDragUpdate(DragUpdateEvent event) => position += event.delta;
@override
void onDragEnd(DragEndEvent event) => _isDragged = false;
@override
void render(Canvas canvas) {
_paint.color = _isDragged? Colors.red : Colors.white;
canvas.drawRect(size.toRect(), _paint);
}
}
HasDraggablesBridge¶
This marker mixin can be used to indicate that the game has both the “new-style” components that
use the DragCallbacks
mixin, and the “old-style” components that use the Draggable
mixin. In
effect, every drag event will be propagated twice through the system: first trying to reach the
components with DragCallbacks
mixin, and then components with Draggable
.
class MyGame extends FlameGame with HasDraggableComponents, HasDraggablesBridge {
// ...
}
The purpose of this mixin is to ease the transition from the old event delivery system to the
new one. With this mixin, you can transition your Draggable
components into using DragCallbacks
one by one, verifying that your game continues to work at every step.
Use of this mixin for any new project is highly discouraged.