|
| 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