Skip to content

Commit 5369f79

Browse files
committed
add doc
1 parent bf69b48 commit 5369f79

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

doc/flame/inputs/scale_events.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Scale Events
2+
3+
**Scale events** occur when the user moves two fingers in a pinch in, or in a pinch out move.
4+
Only one single scale gesture can occur at the same time.
5+
6+
7+
For those components that you want to respond to scale events, add the `ScaleCallbacks` mixin.
8+
9+
- This mixin adds three overridable methods to your component: `onScaleStart`, `onScaleUpdate`,
10+
`onScaleEnd`. By default, these methods do nothing -- they need to be
11+
overridden in order to perform any function.
12+
- In addition, the component must implement the `containsLocalPoint()` method (already implemented
13+
in `PositionComponent`, so most of the time you don't need to do anything here) -- this method
14+
allows Flame to know whether the event occurred within the component or not.
15+
16+
```dart
17+
class MyComponent extends PositionComponent with ScaleCallbacks {
18+
MyComponent() : super(size: Vector2(180, 120));
19+
20+
@override
21+
void onScaleStart(ScaleStartEvent event) {
22+
// Do something in response to a scale event
23+
}
24+
}
25+
```
26+
27+
28+
## Scale anatomy
29+
30+
31+
### onScaleStart
32+
33+
This is the first event that occurs in a scale sequence. Usually, the event will be delivered to the
34+
topmost component at the focal point (the point at the center of the line formed by the two fingers)
35+
with the `ScaleCallbacks` mixin. However, by setting the flag
36+
`event.continuePropagation` to true, you can allow the event to propagate to the components below.
37+
38+
The `ScaleStartEvent` object associated with this event will contain the coordinate of the first focal point
39+
recognised by the scale gesture recogniser. This point is available in multiple coordinate system:
40+
`devicePosition` is given in the coordinate system of the entire device, `canvasPosition` is in the
41+
coordinate system of the game widget, and `localPosition` provides the position in the component's
42+
local coordinate system.
43+
44+
Any component that receives `onScaleStart` will later be receiving `onScaleUpdate` and `onScaleEnd`
45+
events as well.
46+
47+
48+
### onScaleUpdate
49+
50+
This event is fired continuously as user drags their finger across the screen. It will not fire if
51+
the user is holding their finger still.
52+
53+
The default implementation delivers this event to all the components that received the previous
54+
`onScaleStart`. If the point of touch is still within the component, then
55+
`event.localPosition` will give the position of that point in the local coordinate system. However,
56+
if the user moves their finger away from the component, the property `event.localPosition` will
57+
return a point whose coordinates are NaNs. Likewise, the `event.renderingTrace` in this case will be
58+
empty. However, the `canvasPosition` and `devicePosition` properties of the event will be valid.
59+
60+
In addition, the `ScaleUpdateEvent` will contain `focalPointDelta` -- the amount the focal point has moved since the
61+
previous `onScaleUpdate`, or since the `onScaleStart` if this is the first scale-update after a scale-
62+
start.
63+
64+
The `event.timestamp` property measures the time elapsed since the beginning of the scale. It can be
65+
used, for example, to compute the speed of the movement.
66+
67+
The `event.rotation` property measures the angle of rotation in radians, between the line formed
68+
from the two fingers at the start, and the line formed when this event is called.
69+
70+
The `event.scale` property measures the ratio of length between the line formed
71+
from the two fingers at the start, and the line formed when this event is called.
72+
73+
74+
### onScaleEnd
75+
76+
This event is fired when the user lifts their finger and thus stops the scale gesture. There is no
77+
position associated with this event.
78+
79+
## Mixins
80+
81+
82+
### ScaleCallbacks
83+
84+
The `ScaleCallbacks` mixin can be added to any `Component` in order for that component to start
85+
receiving scale events.
86+
87+
This mixin adds methods `onScaleStart`, `onScaleUpdate`, `onScaleEnd` to the
88+
component, which by default don't do anything, but can be overridden to implement any real
89+
functionality.
90+
91+
Another crucial detail is that a component will only receive scale events that originate *within*
92+
that component, as judged by the `containsLocalPoint()` function. The commonly-used
93+
`PositionComponent` class provides such an implementation based on its `size` property. Thus, if
94+
your component derives from a `PositionComponent`, then make sure that you set its size correctly.
95+
If, however, your component derives from the bare `Component`, then the `containsLocalPoint()`
96+
method must be implemented manually.
97+
98+
If your component is a part of a larger hierarchy, then it will only receive scale events if its
99+
ancestors have all implemented the `containsLocalPoint` correctly.
100+
101+
```dart
102+
class ScaleOnlyRectangle extends RectangleComponent with ScaleCallbacks {
103+
ScaleOnlyRectangle({
104+
required Vector2 position,
105+
required Vector2 size,
106+
Color color = Colors.blue,
107+
Anchor anchor = Anchor.center,
108+
}) : super(
109+
position: position,
110+
size: size,
111+
anchor: anchor,
112+
paint: Paint()..color = color,
113+
);
114+
115+
@override
116+
Future<void> onLoad() async {
117+
final text = TextComponent(
118+
text: 'scale',
119+
textRenderer: TextPaint(
120+
style: const TextStyle(fontSize: 25, color: Colors.white),
121+
),
122+
position: size / 2,
123+
anchor: Anchor.center,
124+
);
125+
add(text);
126+
}
127+
128+
bool isScaling = false;
129+
double initialAngle = 0;
130+
Vector2 initialScale = Vector2.all(1);
131+
double lastScale = 1.0;
132+
133+
/// ScaleCallbacks overrides
134+
@override
135+
void onScaleStart(ScaleStartEvent event) {
136+
super.onScaleStart(event);
137+
isScaling = true;
138+
initialAngle = angle;
139+
initialScale = scale;
140+
lastScale = 1.0;
141+
debugPrint('Scale started at ${event.devicePosition}');
142+
}
143+
144+
@override
145+
void onScaleUpdate(ScaleUpdateEvent event) {
146+
super.onScaleUpdate(event);
147+
// scale rectangle size by pinch
148+
angle = initialAngle + event.rotation;
149+
// delta scale since last frame
150+
if (lastScale == 0) {
151+
return;
152+
}
153+
final scaleDelta = event.scale / lastScale;
154+
lastScale = event.scale; // update for next frame
155+
156+
// apply delta gently
157+
scale *= sqrt(scaleDelta);
158+
159+
// clamp
160+
scale.clamp(Vector2.all(0.8), Vector2.all(3));
161+
}
162+
163+
@override
164+
void onScaleEnd(ScaleEndEvent event) {
165+
super.onScaleEnd(event);
166+
isScaling = false;
167+
debugPrint('Scale ended with velocity ${event.velocity}');
168+
}
169+
}
170+
171+
```
172+
173+
## Scale and drag gestures interactions
174+
175+
A multi drag gesture can sometimes look exactly like a scale gesture. This is the case for instance, if you try to move two components toward each other at the same time.
176+
If you added both a component using ScaleCallbacks and one using DragCallbacks (or one using both), this issue will arise. The Scale gesture will win over the drag gesture
177+
and prevent your user to perform the multi drag gesture as they wanted. This is a limitation with the current implementation that devs need to be aware of.

0 commit comments

Comments
 (0)