Tap events


This document describes a new experimental API. The more traditional approach for handling tap events is described in Gesture Input.

Tap events are one of the most basic methods of interaction with a Flame game. These events occur when the user touches the screen with a finger, or clicks with a mouse, or taps with a stylus. A tap can be “long”, but the finger isn’t supposed to move during the gesture. Thus, touching the screen, then moving the finger, and then releasing – is not a tap but a drag. Similarly, clicking a mouse button while the mouse is moving will also be registered as a drag.

Multiple tap events can occur at the same time, especially if the user has multiple fingers. Such cases will be handled correctly by Flame, and you can even keep track of the events by using their pointerId property.

It takes only a few simple steps to enable these events for your game:

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

    class MyGame extends FlameGame with HasTappableComponents {
      // ...
  2. For those components that you want to respond to taps, add the TapCallbacks mixin.

    • This mixin adds four overridable methods to your component: onTapDown, onTapUp, onTapCancel, and onLongTapDown. By default, each of these methods does 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 TapCallbacks {
      MyComponent() : super(size: Vector2(80, 60));
      void onTapUp(TapUpEvent event) {
        // Do something in response to a tap

Tap anatomy


Every tap begins with a “tap down” event, which you receive via the void onTapDown(TapDownEvent) handler. The event is delivered to the first component located at the point of touch that has the TapCallbacks mixin. Normally, the event then stops propagation. However, you can force the event to also be delivered to the components below by setting event.continuePropagation to true.

The TapDownEvent object that is passed to the event handler, contains the available information about the event. For example, event.localPosition will contain the coordinate of the event in the current component’s local coordinate system, whereas event.canvasPosition is in the coordinate system of the entire game canvas.

Every component that received an onTapDown event will eventually receive either onTapUp or onTapCancel with the same pointerId.


If the user holds their finger down for some time (as configured by the .longTapDelay property in HasTappableComponents), the “long tap” will be generated. This event invokes the void onLongTapDown(TapDownEvent) handler on those components that previously received the onTapDown event.


This event indicates successful completion of the tap sequence. It is guaranteed to only be delivered to those components that previously received the onTapDown event with the same pointer id.

The TapUpEvent object passed to the event handler contains the information about the event, which includes the coordinate of the event (i.e. where the user was touching the screen right before lifting their finger), and the event’s pointerId.

Note that the device coordinates of the tap-up event will be the same (or very close) to the device coordinates of the corresponding tap-down event. However, the same cannot be said about the local coordinates. If the component that you’re tapping is moving (as they often tend to in games), then you may find that the local tap-up coordinates are quite different from the local tap-down coordinates.

In extreme case, when the component moves away from the point of touch, the onTapUp event will not be generated at all: it will be replaced with onTapCancel. Note, however, that in this case the onTapCancel will be generated at the moment the user lifts or moves their finger, not at the moment the component moves away from the point of touch.


This event occurs when the tap fails to materialize. Most often, this will happen if the user moves their finger, which converts the gesture from “tap” into “drag”. Less often, this may happen when the component being tapped moves away from under the user’s finger. Even more rarely, the onTapCancel occurs when another widget pops over the game widget, or when the device turns off, or similar situations.

The TapCancelEvent object contains only the pointerId of the previous TapDownEvent which is now being canceled. There is no position associated with a tap-cancel.


Play with the demo below to see the tap events in action.

The blue-ish rectangle in the middle is the component that has the TapCallbacks mixin. Tapping this component would create circles at the points of touch. Specifically, onTapDown event starts making the circle. The thickness of the circle will be proportional to the duration of the tap: after onTapUp the circle’s stroke width will no longer grow. There will be a thin white stripe at the moment the onLongTapDown fires. Lastly, the circle will implode and disappear if you cause the onTapCancel event by moving the finger.

  1import 'dart:math';
  3import 'package:flame/components.dart';
  4import 'package:flame/experimental.dart';
  5import 'package:flame/game.dart';
  6import 'package:flutter/rendering.dart';
  8/// The main [FlameGame] class uses [HasTappableComponents] in order to enable
  9/// tap events propagation.
 10class TapEventsGame extends FlameGame with HasTappableComponents {
 11  @override
 12  Future<void> onLoad() async {
 13    add(TapTarget());
 14  }
 17/// This component is the tappable blue-ish rectangle in the center of the
 18/// game. It uses the [TapCallbacks] mixin in order to inform the game that it
 19/// wants to receive tap events.
 20class TapTarget extends PositionComponent with TapCallbacks {
 21  TapTarget() : super(anchor: Anchor.center);
 23  final _paint = Paint()..color = const Color(0x448BA8FF);
 25  /// We will store all current circles into this map, keyed by the `pointerId`
 26  /// of the event that created the circle.
 27  final Map<int, ExpandingCircle> _circles = {};
 29  @override
 30  void onGameResize(Vector2 canvasSize) {
 31    super.onGameResize(canvasSize);
 32    size = canvasSize - Vector2(100, 75);
 33    if (size.x < 100 || size.y < 100) {
 34      size = canvasSize * 0.9;
 35    }
 36    position = canvasSize / 2;
 37  }
 39  @override
 40  void render(Canvas canvas) {
 41    canvas.drawRect(size.toRect(), _paint);
 42  }
 44  @override
 45  void onTapDown(TapDownEvent event) {
 46    final circle = ExpandingCircle(event.localPosition);
 47    _circles[event.pointerId] = circle;
 48    add(circle);
 49  }
 51  @override
 52  void onLongTapDown(TapDownEvent event) {
 53    _circles[event.pointerId]!.accent();
 54  }
 56  @override
 57  void onTapUp(TapUpEvent event) {
 58    _circles.remove(event.pointerId)!.release();
 59  }
 61  @override
 62  void onTapCancel(TapCancelEvent event) {
 63    _circles.remove(event.pointerId)!.cancel();
 64  }
 67class ExpandingCircle extends Component {
 68  ExpandingCircle(this._center)
 69      : _baseColor =
 70            HSLColor.fromAHSL(1, random.nextDouble() * 360, 1, 0.8).toColor();
 72  final Color _baseColor;
 73  final Vector2 _center;
 74  double _outerRadius = 0;
 75  double _innerRadius = 0;
 76  bool _released = false;
 77  bool _cancelled = false;
 78  late final _paint = Paint()
 79    ..style = PaintingStyle.stroke
 80    ..color = _baseColor;
 82  /// "Accent" is thin white circle generated by `onLongTapDown`. We use
 83  /// negative radius to indicate that the circle should not be drawn yet.
 84  double _accentRadius = -1e10;
 85  late final _accentPaint = Paint()
 86    ..style = PaintingStyle.stroke
 87    ..strokeWidth = 0
 88    ..color = const Color(0xFFFFFFFF);
 90  /// At this radius the circle will disappear.
 91  static const maxRadius = 175;
 92  static final random = Random();
 94  double get radius => (_innerRadius + _outerRadius) / 2;
 96  void release() => _released = true;
 97  void cancel() => _cancelled = true;
 98  void accent() => _accentRadius = 0;
100  @override
101  void render(Canvas canvas) {
102    canvas.drawCircle(_center.toOffset(), radius, _paint);
103    if (_accentRadius >= 0) {
104      canvas.drawCircle(_center.toOffset(), _accentRadius, _accentPaint);
105    }
106  }
108  @override
109  void update(double dt) {
110    if (_cancelled) {
111      _innerRadius += dt * 100; // implosion
112    } else {
113      _outerRadius += dt * 20;
114      _innerRadius += dt * (_released ? 20 : 6);
115      _accentRadius += dt * 20;
116    }
117    if (radius >= maxRadius || _innerRadius > _outerRadius) {
118      removeFromParent();
119    } else {
120      final opacity = 1 - radius / maxRadius;
121      _paint.color = _baseColor.withOpacity(opacity);
122      _paint.strokeWidth = _outerRadius - _innerRadius;
123    }
124  }


This section describes in more details several mixins needed for tap event handling.


This mixin is used on a FlameGame in order to ensure that tap events coming from Flutter reach their target Components. This mixin must be added if you have any components with the TapCallbacks mixin.

The mixin adds methods onTapDown, onLongTapDown, onTapUp, and onTapCancel 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 HasTappableComponents {
  void onTapDown(TapDownEvent event) {
    if (!event.handled) {
      print('Event $event was not handled by any component');


The TapCallbacks mixin can be added to any Component in order for that component to start receiving tap events.

This mixin adds methods onTapDown, onLongTapDown, onTapUp, and onTapCancel to the component, which by default don’t do anything, but can be overridden to implement any real functionality. There is no need to override all of them either: for example, you can override only onTapUp if you wish to respond to “real” taps only.

Another crucial detail is that a component will only receive tap events that occur 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 tap events if its parent has implemented the containsLocalPoint correctly.

class MyComponent extends Component with TapCallbacks {
  final _rect = const Rect.fromLTWH(0, 0, 100, 100);
  final _paint = Paint();
  bool _isPressed = false;

  bool containsLocalPoint(Vector2 point) => _rect.contains(point.toOffset());

  void onTapDown(TapDownEvent event) => _isPressed = true;

  void onTapUp(TapUpEvent event) => _isPressed = false;

  void onTapCancel(TapCancelEvent event) => _isPressed = false;

  void render(Canvas canvas) {
    _paint.color = _isPressed? Colors.red : Colors.white;
    canvas.drawRect(_rect, _paint);


This marker mixin can be used to indicate that the game has both the “new-style” components that use the TapCallbacks mixin, and the “old-style” components that use the Tappable mixin. In effect, every tap event will be propagated twice through the system: first trying to reach the components with TapCallbacks mixin, and then components with Tappable.

class MyGame extends FlameGame with HasTappableComponents, HasTappablesBridge {
  // ...

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 Tappable components into using TapCallbacks one by one, verifying that your game continues to work at every step.

Use of this mixin for any new project is highly discouraged.


If you have an existing game that uses Tappable/HasTappables mixins, then this section will describe how to transition to the new API described in this document. Here’s what you need to do:

  1. Replace the HasTappables mixin on your game with the pair of mixins HasTappableComponents, HasTappablesBridge. Verify that your game continues to run as before.

  2. Pick any of your components that uses Tappable, and replace that mixin with TapCallbacks. The methods onTapDown, onTapUp, onTapCancel and onLongTapDown will need to be adjusted for the new API:

    • The argument pair such as (int pointerId, TapDownDetails details) was replaced with a single event object TapDownEvent event.

    • There is no return value anymore, but if you need to make a component to pass-through the taps to the components below, then set event.continuePropagation to true. This is only needed for onTapDown events – all other events will pass-through automatically.

    • If your component needs to know the coordinates of the point of touch, use event.localPosition instead of computing it manually. Properties event.canvasPosition and event.devicePosition are also available.

    • If the component is a PositionComponent, then make sure its size is set correctly (for example by turning on the debug mode). If the component does not derive from PositionComponent then make sure it implements the method containsLocalPoint().

    • If the component is not attached to the root of the game, then make sure its ancestors also have correct size or implement containsLocalPoint().

  3. Run the game to verify that it works as before.

  4. Repeat step 2 until you have converted all Tappable mixins into TapCallbacks.

  5. Remove the HasTappablesBridge mixin from your top-level game.