Drag Events¶
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.
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 (already implemented inPositionComponent
, so most of the time you don’t need to do anything here) – 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 magenta rectangle.
1import 'dart:math';
2
3import 'package:flame/components.dart';
4import 'package:flame/events.dart';
5import 'package:flame/game.dart';
6import 'package:flame/geometry.dart';
7import 'package:flutter/rendering.dart';
8
9class DragEventsGame extends FlameGame {
10 @override
11 Future<void> onLoad() async {
12 addAll([
13 DragTarget(),
14 Star(
15 n: 5,
16 radius1: 40,
17 radius2: 20,
18 sharpness: 0.2,
19 color: const Color(0xffbae5ad),
20 position: Vector2(70, 70),
21 ),
22 Star(
23 n: 3,
24 radius1: 50,
25 radius2: 40,
26 sharpness: 0.3,
27 color: const Color(0xff6ecbe5),
28 position: Vector2(70, 160),
29 ),
30 Star(
31 n: 12,
32 radius1: 10,
33 radius2: 75,
34 sharpness: 1.3,
35 color: const Color(0xfff6df6a),
36 position: Vector2(70, 270),
37 ),
38 Star(
39 n: 10,
40 radius1: 20,
41 radius2: 17,
42 sharpness: 0.85,
43 color: const Color(0xfff82a4b),
44 position: Vector2(110, 110),
45 ),
46 ]);
47 }
48}
49
50/// This component is the pink-ish rectangle in the center of the game window.
51/// It uses the [DragCallbacks] mixin in order to receive drag events.
52class DragTarget extends PositionComponent with DragCallbacks {
53 DragTarget() : super(anchor: Anchor.center);
54
55 final _rectPaint = Paint()..color = const Color(0x88AC54BF);
56
57 /// We will store all current circles into this map, keyed by the `pointerId`
58 /// of the event that created the circle.
59 final Map<int, Trail> _trails = {};
60
61 @override
62 void onGameResize(Vector2 size) {
63 super.onGameResize(size);
64 this.size = size - Vector2(100, 75);
65 if (this.size.x < 100 || this.size.y < 100) {
66 this.size = size * 0.9;
67 }
68 position = size / 2;
69 }
70
71 @override
72 void render(Canvas canvas) {
73 canvas.drawRect(size.toRect(), _rectPaint);
74 }
75
76 @override
77 void onDragStart(DragStartEvent event) {
78 super.onDragStart(event);
79 final trail = Trail(event.localPosition);
80 _trails[event.pointerId] = trail;
81 add(trail);
82 }
83
84 @override
85 void onDragUpdate(DragUpdateEvent event) {
86 _trails[event.pointerId]!.addPoint(event.localEndPosition);
87 }
88
89 @override
90 void onDragEnd(DragEndEvent event) {
91 super.onDragEnd(event);
92 _trails.remove(event.pointerId)!.end();
93 }
94
95 @override
96 void onDragCancel(DragCancelEvent event) {
97 super.onDragCancel(event);
98 _trails.remove(event.pointerId)!.cancel();
99 }
100}
101
102class Trail extends Component {
103 Trail(Vector2 origin)
104 : _paths = [Path()..moveTo(origin.x, origin.y)],
105 _opacities = [1],
106 _lastPoint = origin.clone(),
107 _color = HSLColor.fromAHSL(
108 1,
109 random.nextDouble() * 360,
110 1,
111 0.8,
112 ).toColor();
113
114 final List<Path> _paths;
115 final List<double> _opacities;
116 Color _color;
117 late final _linePaint = Paint()..style = PaintingStyle.stroke;
118 late final _circlePaint = Paint()..color = _color;
119 bool _released = false;
120 double _timer = 0;
121 final _vanishInterval = 0.03;
122 final Vector2 _lastPoint;
123
124 static final random = Random();
125 static const lineWidth = 10.0;
126
127 @override
128 void render(Canvas canvas) {
129 assert(_paths.length == _opacities.length);
130 for (var i = 0; i < _paths.length; i++) {
131 final path = _paths[i];
132 final opacity = _opacities[i];
133 if (opacity > 0) {
134 _linePaint.color = _color.withValues(alpha: opacity);
135 _linePaint.strokeWidth = lineWidth * opacity;
136 canvas.drawPath(path, _linePaint);
137 }
138 }
139 canvas.drawCircle(
140 _lastPoint.toOffset(),
141 (lineWidth - 2) * _opacities.last + 2,
142 _circlePaint,
143 );
144 }
145
146 @override
147 void update(double dt) {
148 assert(_paths.length == _opacities.length);
149 _timer += dt;
150 while (_timer > _vanishInterval) {
151 _timer -= _vanishInterval;
152 for (var i = 0; i < _paths.length; i++) {
153 _opacities[i] -= 0.01;
154 if (_opacities[i] <= 0) {
155 _paths[i].reset();
156 }
157 }
158 if (!_released) {
159 _paths.add(Path()..moveTo(_lastPoint.x, _lastPoint.y));
160 _opacities.add(1);
161 }
162 }
163 if (_opacities.last < 0) {
164 removeFromParent();
165 }
166 }
167
168 void addPoint(Vector2 point) {
169 if (!point.x.isNaN) {
170 for (final path in _paths) {
171 path.lineTo(point.x, point.y);
172 }
173 _lastPoint.setFrom(point);
174 }
175 }
176
177 void end() => _released = true;
178
179 void cancel() {
180 _released = true;
181 _color = const Color(0xFFFFFFFF);
182 }
183}
184
185class Star extends PositionComponent with DragCallbacks {
186 Star({
187 required int n,
188 required double radius1,
189 required double radius2,
190 required double sharpness,
191 required this.color,
192 super.position,
193 }) {
194 _path = Path()..moveTo(radius1, 0);
195 for (var i = 0; i < n; i++) {
196 final p1 = Vector2(radius2, 0)..rotate(tau / n * (i + sharpness));
197 final p2 = Vector2(radius2, 0)..rotate(tau / n * (i + 1 - sharpness));
198 final p3 = Vector2(radius1, 0)..rotate(tau / n * (i + 1));
199 _path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
200 }
201 _path.close();
202 }
203
204 final Color color;
205 final Paint _paint = Paint();
206 final Paint _borderPaint = Paint()
207 ..color = const Color(0xFFffffff)
208 ..style = PaintingStyle.stroke
209 ..strokeWidth = 3;
210 final _shadowPaint = Paint()
211 ..color = const Color(0xFF000000)
212 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
213 late final Path _path;
214
215 @override
216 bool containsLocalPoint(Vector2 point) {
217 return _path.contains(point.toOffset());
218 }
219
220 @override
221 void render(Canvas canvas) {
222 if (isDragged) {
223 _paint.color = color.withValues(alpha: 0.5);
224 canvas.drawPath(_path, _paint);
225 canvas.drawPath(_path, _borderPaint);
226 } else {
227 _paint.color = color.withValues(alpha: 1);
228 canvas.drawPath(_path, _shadowPaint);
229 canvas.drawPath(_path, _paint);
230 }
231 }
232
233 @override
234 void onDragStart(DragStartEvent event) {
235 super.onDragStart(event);
236 priority = 10;
237 }
238
239 @override
240 void onDragEnd(DragEndEvent event) {
241 super.onDragEnd(event);
242 priority = 0;
243 }
244
245 @override
246 void onDragUpdate(DragUpdateEvent event) {
247 position += event.localDelta;
248 }
249}
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¶
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);
}
}