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, and onDragCancel. 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 in PositionComponent, 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.

drag_events.dart
  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.

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

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

This event is fired when the drag gesture is interrupted before it ends naturally, for example when another gesture recognizer wins the gesture arena or a second pointer triggers a scale takeover. Unlike onDragEnd it carries no velocity information. The default implementation simply resets the drag state; override it and call onDragEnd(event.toDragEnd()) yourself if you want a cancellation handled identically to a natural drag end.

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.

isDragged

The DragCallbacks mixin provides an isDragged getter that returns true while the component is actively being dragged. This is set to true at onDragStart and back to false at onDragEnd. It can be used, for example, to change the component’s visual appearance during a drag.

allowsMultiPointerDrag

Drags are tracked per pointer, so a component that is already being dragged will start a second, independent drag when another finger touches it. That is what you want when each drag manipulates something of its own, but not when they all drive a single piece of state (such as a camera or a draggable object), where a second finger just fights the first.

Override allowsMultiPointerDrag to false to accept only one drag at a time:

class MagnifyingGlass extends PositionComponent with DragCallbacks {
  @override
  bool get allowsMultiPointerDrag => false;

  @override
  void onDragUpdate(DragUpdateEvent event) {
    position = event.canvasEndPosition;
  }
}

While a drag is in progress, no other pointer gets an onDragStart on this component, and no onDragUpdate, onDragEnd or onDragCancel follow for it either; the event is offered to the components below instead. Once the accepted drag ends or is cancelled, the component is free to accept a new one.

Control is not handed over: if the accepted pointer is lifted while another is still down, the drag ends rather than continuing on the remaining finger.

This only gates drags. A component that also uses ScaleCallbacks keeps receiving scale events normally, so one-finger drag plus two-finger pinch still works.

Combining with ScaleCallbacks

DragCallbacks and ScaleCallbacks can be used at the same time: single-finger gestures produce drag events, and two-finger gestures produce both drag and scale events. See Combining with DragCallbacks for how to make the two work together, both on a component and for panning and zooming the camera.