Tap Events¶
Note
This document describes the new events API. The old (legacy) approach, which is still supported, 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.
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
, andonLongTapDown
. 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 (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 TapCallbacks {
MyComponent() : super(size: Vector2(80, 60));
@override
void onTapUp(TapUpEvent event) {
// Do something in response to a tap event
}
}
Tap anatomy¶
onTapDown¶
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
.
onLongTapDown¶
If the user holds their finger down for some time (as configured by the .longTapDelay
property
in MultiTapDispatcher
), “long tap” will be triggered. This event invokes the
void onLongTapDown(TapDownEvent)
handler on those components that previously received the
onTapDown
event.
By default, the .longTapDelay
is set to 300 milliseconds, what may be different of the system default.
You can change this value by setting the TapConfig.longTapDelay
value.
It may also be useful for specific accessibility needs.
onTapUp¶
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.
onTapCancel¶
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.
Demo¶
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';
2
3import 'package:flame/components.dart';
4import 'package:flame/events.dart';
5import 'package:flame/game.dart';
6import 'package:flutter/rendering.dart';
7
8class TapEventsGame extends FlameGame {
9 @override
10 Future<void> onLoad() async {
11 add(TapTarget());
12 }
13}
14
15/// This component is the tappable blue-ish rectangle in the center of the game.
16/// It uses the [TapCallbacks] mixin to receive tap events.
17class TapTarget extends PositionComponent with TapCallbacks {
18 TapTarget() : super(anchor: Anchor.center);
19
20 final _paint = Paint()..color = const Color(0x448BA8FF);
21
22 /// We will store all current circles into this map, keyed by the `pointerId`
23 /// of the event that created the circle.
24 final Map<int, ExpandingCircle> _circles = {};
25
26 @override
27 void onGameResize(Vector2 size) {
28 super.onGameResize(size);
29 this.size = size - Vector2(100, 75);
30 if (this.size.x < 100 || this.size.y < 100) {
31 this.size = size * 0.9;
32 }
33 position = size / 2;
34 }
35
36 @override
37 void render(Canvas canvas) {
38 canvas.drawRect(size.toRect(), _paint);
39 }
40
41 @override
42 void onTapDown(TapDownEvent event) {
43 final circle = ExpandingCircle(event.localPosition);
44 _circles[event.pointerId] = circle;
45 add(circle);
46 }
47
48 @override
49 void onLongTapDown(TapDownEvent event) {
50 _circles[event.pointerId]!.accent();
51 }
52
53 @override
54 void onTapUp(TapUpEvent event) {
55 _circles.remove(event.pointerId)!.release();
56 }
57
58 @override
59 void onTapCancel(TapCancelEvent event) {
60 _circles.remove(event.pointerId)!.cancel();
61 }
62}
63
64class ExpandingCircle extends Component {
65 ExpandingCircle(this._center)
66 : _baseColor =
67 HSLColor.fromAHSL(1, random.nextDouble() * 360, 1, 0.8).toColor();
68
69 final Color _baseColor;
70 final Vector2 _center;
71 double _outerRadius = 0;
72 double _innerRadius = 0;
73 bool _released = false;
74 bool _cancelled = false;
75 late final _paint = Paint()
76 ..style = PaintingStyle.stroke
77 ..color = _baseColor;
78
79 /// "Accent" is thin white circle generated by `onLongTapDown`. We use
80 /// negative radius to indicate that the circle should not be drawn yet.
81 double _accentRadius = -1e10;
82 late final _accentPaint = Paint()
83 ..style = PaintingStyle.stroke
84 ..strokeWidth = 0
85 ..color = const Color(0xFFFFFFFF);
86
87 /// At this radius the circle will disappear.
88 static const maxRadius = 175;
89 static final random = Random();
90
91 double get radius => (_innerRadius + _outerRadius) / 2;
92
93 void release() => _released = true;
94 void cancel() => _cancelled = true;
95 void accent() => _accentRadius = 0;
96
97 @override
98 void render(Canvas canvas) {
99 canvas.drawCircle(_center.toOffset(), radius, _paint);
100 if (_accentRadius >= 0) {
101 canvas.drawCircle(_center.toOffset(), _accentRadius, _accentPaint);
102 }
103 }
104
105 @override
106 void update(double dt) {
107 if (_cancelled) {
108 _innerRadius += dt * 100; // implosion
109 } else {
110 _outerRadius += dt * 20;
111 _innerRadius += dt * (_released ? 20 : 6);
112 _accentRadius += dt * 20;
113 }
114 if (radius >= maxRadius || _innerRadius > _outerRadius) {
115 removeFromParent();
116 } else {
117 final opacity = 1 - radius / maxRadius;
118 _paint.color = _baseColor.withOpacity(opacity);
119 _paint.strokeWidth = _outerRadius - _innerRadius;
120 }
121 }
122}
Mixins¶
This section describes in more details several mixins needed for tap event handling.
TapCallbacks¶
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;
@override
bool containsLocalPoint(Vector2 point) => _rect.contains(point.toOffset());
@override
void onTapDown(TapDownEvent event) => _isPressed = true;
@override
void onTapUp(TapUpEvent event) => _isPressed = false;
@override
void onTapCancel(TapCancelEvent event) => _isPressed = false;
@override
void render(Canvas canvas) {
_paint.color = _isPressed? Colors.red : Colors.white;
canvas.drawRect(_rect, _paint);
}
}
DoubleTapCallbacks¶
Flame also offers a mixin named DoubleTapCallbacks
to receive a double-tap event from the
component. To start receiving double tap events in a component, add the
DoubleTapCallbacks
mixin to your PositionComponent
.
class MyComponent extends PositionComponent with DoubleTapCallbacks {
@override
void onDoubleTapUp(DoubleTapEvent event) {
/// Do something
}
@override
void onDoubleTapCancel(DoubleTapCancelEvent event) {
/// Do something
}
@override
void onDoubleTapDown(DoubleTapDownEvent event) {
/// Do something
}
Migration¶
If you have an existing game that uses Tappable
/Draggable
mixins, then this section will
describe how to transition to the new API described in this document. Here’s what you need to do:
Take all of your components that uses these mixins, and replace them with
TapCallbacks
/DragCallbacks
.
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 objectTapDownEvent 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 foronTapDown
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. Propertiesevent.canvasPosition
andevent.devicePosition
are also available.If the component is attached to a custom ancestor then make sure that ancestor also have the correct size or implement
containsLocalPoint()
.