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:

  1. Add the HasDraggableComponents mixin to your main game class:

    class MyGame extends FlameGame with HasDraggableComponents {
     // ...
    }
    
  2. 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 – 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.

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