mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
fix!: Add DisplacementEvent to fix delta coordinate transformations for drag events (#2871)
This adds `DisplacementEvent` to fix delta coordinate transformations
for drag events, to be used instead of `PositionEvent`.
Drag Events now expose the start and end position, as well as the delta,
correctly transformed by the camera and zoom.
This also ensures that drag events, once starts, do not get lost if the
drag update leaves the component bounds.
* if you are using `DragUpdateEvent` events, the `devicePosition`,
`canvasPosition`, `localPosition`, and `delta` are deprecated as they
are unclear.
* use `xStartPosition` to get the position at the start of the drag
event ("from")
* use `xEndPosition` to get the position at the end of the drag event
("to")
* if you want the delta, use `localDelta`. it now already considers the
camera zoom. no need to manually account for that
* now you keep receiving drag events for the same component even if the
mouse leaves the component (breaking)
---------
Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
@ -83,7 +83,7 @@ class DragTarget extends PositionComponent with DragCallbacks {
|
||||
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent event) {
|
||||
_trails[event.pointerId]!.addPoint(event.localPosition);
|
||||
_trails[event.pointerId]!.addPoint(event.localEndPosition);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -240,6 +240,6 @@ class Star extends PositionComponent with DragCallbacks {
|
||||
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent event) {
|
||||
position += event.delta;
|
||||
position += event.localDelta;
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,8 +241,7 @@ class Card extends PositionComponent with DragCallbacks {
|
||||
if (!_isDragging) {
|
||||
return;
|
||||
}
|
||||
final cameraZoom = findGame()!.camera.viewfinder.zoom;
|
||||
final delta = event.delta / cameraZoom;
|
||||
final delta = event.localDelta;
|
||||
position.add(delta);
|
||||
attachedCards.forEach((card) => card.position.add(delta));
|
||||
}
|
||||
|
||||
@ -265,8 +265,7 @@ class Card extends PositionComponent
|
||||
if (!_isDragging) {
|
||||
return;
|
||||
}
|
||||
final cameraZoom = findGame()!.camera.viewfinder.zoom;
|
||||
final delta = event.delta / cameraZoom;
|
||||
final delta = event.localDelta;
|
||||
position.add(delta);
|
||||
attachedCards.forEach((card) => card.position.add(delta));
|
||||
}
|
||||
|
||||
@ -502,19 +502,13 @@ the card, so that it is rendered above all others. Without this, the card would
|
||||
During the drag, the `onDragUpdate` event will be called continuously. Using this callback we will
|
||||
be updating the position of the card so that it follows the movement of the finger (or the mouse).
|
||||
The `event` object passed to this callback contains the most recent coordinate of the point of
|
||||
touch, and also the `delta` property -- which is the displacement vector since the previous call of
|
||||
`onDragUpdate`. The only problem is that this delta is measured in screen pixels, whereas we want
|
||||
it to be in game world units. The conversion between the two is given by the camera zoom level, so
|
||||
we will add an extra method to determine the zoom level:
|
||||
touch, and also the `localDelta` property -- which is the displacement vector since the previous
|
||||
call of `onDragUpdate`, considering the camera zoom.
|
||||
|
||||
```dart
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent event) {
|
||||
final cameraZoom = (findGame()! as FlameGame)
|
||||
.camera
|
||||
.viewfinder
|
||||
.zoom;
|
||||
position += event.delta / cameraZoom;
|
||||
position += event.delta;
|
||||
}
|
||||
```
|
||||
|
||||
@ -606,11 +600,7 @@ to `false`:
|
||||
if (!isDragged) {
|
||||
return;
|
||||
}
|
||||
final cameraZoom = (findGame()! as FlameGame)
|
||||
.camera
|
||||
.viewfinder
|
||||
.zoom;
|
||||
position += event.delta / cameraZoom;
|
||||
position += event.delta;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -940,11 +930,7 @@ the `onDragUpdate` method:
|
||||
if (!isDragged) {
|
||||
return;
|
||||
}
|
||||
final cameraZoom = (findGame()! as FlameGame)
|
||||
.camera
|
||||
.viewfinder
|
||||
.zoom;
|
||||
final delta = event.delta / cameraZoom;
|
||||
final delta = event.delta;
|
||||
position.add(delta);
|
||||
attachedCards.forEach((card) => card.position.add(delta));
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ class DraggableBall extends Ball with DragCallbacks {
|
||||
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent event) {
|
||||
body.applyLinearImpulse(event.delta * 1000);
|
||||
body.applyLinearImpulse(event.localDelta * 1000);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -55,7 +55,7 @@ class MouseJointWorld extends Forge2DWorld
|
||||
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent info) {
|
||||
mouseJoint?.setTarget(info.localPosition);
|
||||
mouseJoint?.setTarget(info.localEndPosition);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -64,10 +64,7 @@ class DraggableBox extends Box with DragCallbacks {
|
||||
|
||||
@override
|
||||
bool onDragUpdate(DragUpdateEvent info) {
|
||||
final target = info.localPosition;
|
||||
if (target.isNaN) {
|
||||
return false;
|
||||
}
|
||||
final target = info.localEndPosition;
|
||||
final mouseJointDef = MouseJointDef()
|
||||
..maxForce = body.mass * 300
|
||||
..dampingRatio = 0
|
||||
|
||||
@ -39,7 +39,6 @@ class DraggableEmber extends Ember with DragCallbacks {
|
||||
|
||||
@override
|
||||
void onDragUpdate(DragUpdateEvent event) {
|
||||
event.continuePropagation = true;
|
||||
return;
|
||||
position += event.localDelta;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';
|
||||
import 'package:flame/src/camera/behaviors/follow_behavior.dart';
|
||||
@ -6,9 +7,6 @@ import 'package:flame/src/camera/viewfinder.dart';
|
||||
import 'package:flame/src/camera/viewport.dart';
|
||||
import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';
|
||||
import 'package:flame/src/camera/viewports/max_viewport.dart';
|
||||
import 'package:flame/src/camera/world.dart';
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:flame/src/components/position_component.dart';
|
||||
import 'package:flame/src/effects/controllers/effect_controller.dart';
|
||||
import 'package:flame/src/effects/move_by_effect.dart';
|
||||
import 'package:flame/src/effects/move_effect.dart';
|
||||
@ -223,22 +221,44 @@ class CameraComponent extends Component {
|
||||
return viewport.localToGlobal(viewfinderPosition, output: output);
|
||||
}
|
||||
|
||||
final _viewportPoint = Vector2.zero();
|
||||
|
||||
@override
|
||||
Iterable<Component> componentsAtPoint(
|
||||
Vector2 point, [
|
||||
List<Vector2>? nestedPoints,
|
||||
]) sync* {
|
||||
final viewportPoint = viewport.globalToLocal(point, output: _viewportPoint);
|
||||
yield* viewport.componentsAtPoint(viewportPoint, nestedPoints);
|
||||
Iterable<Component> componentsAtLocation<T>(
|
||||
T locationContext,
|
||||
List<T>? nestedContexts,
|
||||
T? Function(CoordinateTransform, T) transformContext,
|
||||
bool Function(Component, T) checkContains,
|
||||
) sync* {
|
||||
final viewportPoint = transformContext(viewport, locationContext);
|
||||
if (viewportPoint == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield* viewport.componentsAtLocation(
|
||||
viewportPoint,
|
||||
nestedContexts,
|
||||
transformContext,
|
||||
checkContains,
|
||||
);
|
||||
if ((world?.isMounted ?? false) &&
|
||||
currentCameras.length < maxCamerasDepth) {
|
||||
if (viewport.containsLocalPoint(_viewportPoint)) {
|
||||
if (checkContains(viewport, viewportPoint)) {
|
||||
currentCameras.add(this);
|
||||
final worldPoint = viewfinder.transform.globalToLocal(_viewportPoint);
|
||||
yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints);
|
||||
yield* world!.componentsAtPoint(worldPoint, nestedPoints);
|
||||
final worldPoint = transformContext(viewfinder, viewportPoint);
|
||||
if (worldPoint == null) {
|
||||
return;
|
||||
}
|
||||
yield* viewfinder.componentsAtLocation(
|
||||
worldPoint,
|
||||
nestedContexts,
|
||||
transformContext,
|
||||
checkContains,
|
||||
);
|
||||
yield* world!.componentsAtLocation(
|
||||
worldPoint,
|
||||
nestedContexts,
|
||||
transformContext,
|
||||
checkContains,
|
||||
);
|
||||
currentCameras.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,8 +63,8 @@ import 'package:meta/meta.dart';
|
||||
/// [update]d, and then all the components will be [render]ed.
|
||||
///
|
||||
/// You may also need to override [containsLocalPoint] if the component needs to
|
||||
/// respond to tap events or similar; the [componentsAtPoint] may also need to
|
||||
/// be overridden if you have reimplemented [renderTree].
|
||||
/// respond to tap events or similar; the [componentsAtLocation] may also need
|
||||
/// to be overridden if you have reimplemented [renderTree].
|
||||
class Component {
|
||||
Component({
|
||||
Iterable<Component>? children,
|
||||
@ -704,28 +704,65 @@ class Component {
|
||||
Iterable<Component> componentsAtPoint(
|
||||
Vector2 point, [
|
||||
List<Vector2>? nestedPoints,
|
||||
]) sync* {
|
||||
nestedPoints?.add(point);
|
||||
]) {
|
||||
return componentsAtLocation<Vector2>(
|
||||
point,
|
||||
nestedPoints,
|
||||
(transform, point) => transform.parentToLocal(point),
|
||||
(component, point) => component.containsLocalPoint(point),
|
||||
);
|
||||
}
|
||||
|
||||
/// This is a generic implementation of [componentsAtPoint]; refer to those
|
||||
/// docs for context.
|
||||
///
|
||||
/// This will find components intersecting a given location context [T]. The
|
||||
/// context can be a single point or a more complicated structure. How to
|
||||
/// interpret the structure T is determined by the provided lambdas,
|
||||
/// [transformContext] and [checkContains].
|
||||
///
|
||||
/// A simple choice of T would be a simple point (i.e. Vector2). In that case
|
||||
/// transformContext needs to be able to transform a Vector2 on the parent
|
||||
/// coordinate space into the coordinate space of a provided
|
||||
/// [CoordinateTransform]; and [checkContains] must be able to determine if
|
||||
/// a given [Component] "contains" the Vector2 (the definition of "contains"
|
||||
/// will vary and shall be determined by the nature of the chosen location
|
||||
/// context [T]).
|
||||
Iterable<Component> componentsAtLocation<T>(
|
||||
T locationContext,
|
||||
List<T>? nestedContexts,
|
||||
T? Function(CoordinateTransform, T) transformContext,
|
||||
bool Function(Component, T) checkContains,
|
||||
) sync* {
|
||||
nestedContexts?.add(locationContext);
|
||||
if (_children != null) {
|
||||
for (final child in _children!.reversed()) {
|
||||
if (child is IgnoreEvents && child.ignoreEvents) {
|
||||
continue;
|
||||
}
|
||||
Vector2? childPoint = point;
|
||||
T? childPoint = locationContext;
|
||||
if (child is CoordinateTransform) {
|
||||
childPoint = (child as CoordinateTransform).parentToLocal(point);
|
||||
childPoint = transformContext(
|
||||
child as CoordinateTransform,
|
||||
locationContext,
|
||||
);
|
||||
}
|
||||
if (childPoint != null) {
|
||||
yield* child.componentsAtPoint(childPoint, nestedPoints);
|
||||
yield* child.componentsAtLocation(
|
||||
childPoint,
|
||||
nestedContexts,
|
||||
transformContext,
|
||||
checkContains,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
final shouldIgnoreEvents =
|
||||
this is IgnoreEvents && (this as IgnoreEvents).ignoreEvents;
|
||||
if (containsLocalPoint(point) && !shouldIgnoreEvents) {
|
||||
if (checkContains(this, locationContext) && !shouldIgnoreEvents) {
|
||||
yield this;
|
||||
}
|
||||
nestedPoints?.removeLast();
|
||||
nestedContexts?.removeLast();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -113,7 +113,7 @@ class JoystickComponent extends HudMarginComponent with DragCallbacks {
|
||||
|
||||
@override
|
||||
bool onDragUpdate(DragUpdateEvent event) {
|
||||
_unscaledDelta.add(event.delta);
|
||||
_unscaledDelta.add(event.localDelta);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
/// transformation, that is, the transform applies to all children of the
|
||||
/// component equally. If that is not the case (for example, the component does
|
||||
/// different transformations for some of its children), then that component
|
||||
/// must implement [Component.componentsAtPoint] method instead.
|
||||
/// must implement [Component.componentsAtLocation] method instead.
|
||||
///
|
||||
/// The two methods of this interface convert between the parent's coordinate
|
||||
/// space and the local coordinates. The methods may also return `null`,
|
||||
|
||||
@ -3,7 +3,7 @@ import 'package:flame/src/components/core/component.dart';
|
||||
/// This mixin allows a component and all it's descendants to ignore events.
|
||||
///
|
||||
/// Do note that this will also ignore the component and its descendants in
|
||||
/// calls to [Component.componentsAtPoint].
|
||||
/// calls to [Component.componentsAtLocation].
|
||||
///
|
||||
/// If you want to dynamically use this mixin, you can add it and set
|
||||
/// [ignoreEvents] true or false at runtime.
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:flame/src/components/mixins/parent_is_a.dart';
|
||||
import 'package:flame/src/components/position_component.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/components/router_component.dart';
|
||||
import 'package:flame/src/effects/effect.dart';
|
||||
import 'package:flame/src/rendering/decorator.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
/// [Route] is a light-weight component that builds and manages a page.
|
||||
///
|
||||
@ -166,12 +163,19 @@ class Route extends PositionComponent with ParentIsA<RouterComponent> {
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Component> componentsAtPoint(
|
||||
Vector2 point, [
|
||||
List<Vector2>? nestedPoints,
|
||||
]) {
|
||||
Iterable<Component> componentsAtLocation<T>(
|
||||
T locationContext,
|
||||
List<T>? nestedContexts,
|
||||
T? Function(CoordinateTransform, T) transformContext,
|
||||
bool Function(Component, T) checkContains,
|
||||
) {
|
||||
if (isRendered) {
|
||||
return super.componentsAtPoint(point, nestedPoints);
|
||||
return super.componentsAtLocation(
|
||||
locationContext,
|
||||
nestedContexts,
|
||||
transformContext,
|
||||
checkContains,
|
||||
);
|
||||
} else {
|
||||
return const Iterable<Component>.empty();
|
||||
}
|
||||
|
||||
151
packages/flame/lib/src/events/messages/displacement_event.dart
Normal file
151
packages/flame/lib/src/events/messages/displacement_event.dart
Normal file
@ -0,0 +1,151 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/events/messages/location_context_event.dart';
|
||||
import 'package:flame/src/game/game.dart';
|
||||
|
||||
/// Location context for the Displacement Event.
|
||||
///
|
||||
/// This represents a user event that has a start and end locations, as the
|
||||
/// ones that are represented by a [DisplacementEvent].
|
||||
typedef DisplacementContext = ({
|
||||
Vector2 start,
|
||||
Vector2 end,
|
||||
});
|
||||
|
||||
extension DisplacementContextDelta on DisplacementContext {
|
||||
/// Displacement delta
|
||||
Vector2 get delta => end - start;
|
||||
}
|
||||
|
||||
/// Base class for events that contain two points on the screen, representing
|
||||
/// some sort of movement (from a "start" to and "end") and having a "delta".
|
||||
/// These include: drag events and (in the future) pointer move events.
|
||||
///
|
||||
/// This class includes properties that describe both positions where the event
|
||||
/// has occurred (start and end) and the delta (i.e. displacement) represented
|
||||
/// by the event.
|
||||
abstract class DisplacementEvent
|
||||
extends LocationContextEvent<DisplacementContext> {
|
||||
DisplacementEvent(
|
||||
this._game, {
|
||||
required this.deviceStartPosition,
|
||||
required this.deviceEndPosition,
|
||||
});
|
||||
final Game _game;
|
||||
|
||||
@Deprecated(
|
||||
'use deviceStartPosition instead; will be removed in version 1.12.0',
|
||||
)
|
||||
late final Vector2 devicePosition = deviceStartPosition;
|
||||
|
||||
@Deprecated(
|
||||
'use canvasStartPosition instead; will be removed in version 1.12.0',
|
||||
)
|
||||
late final Vector2 canvasPosition = canvasStartPosition;
|
||||
|
||||
@Deprecated(
|
||||
'use localStartPosition instead; will be removed in version 1.12.0',
|
||||
)
|
||||
Vector2 get localPosition => localStartPosition;
|
||||
|
||||
@Deprecated('use localDelta instead; will be removed in version 1.12.0')
|
||||
Vector2 get delta => localDelta;
|
||||
|
||||
/// Event start position in the coordinate space of the device -- either the
|
||||
/// phone, or the browser window, or the app.
|
||||
///
|
||||
/// If the game runs in a full-screen mode, then this would be equal to the
|
||||
/// [canvasStartPosition]. Otherwise, the [deviceStartPosition] is the
|
||||
/// Flutter-level global position.
|
||||
final Vector2 deviceStartPosition;
|
||||
|
||||
/// Event start position in the coordinate space of the game widget, i.e.
|
||||
/// relative to the game canvas.
|
||||
///
|
||||
/// This could be considered the Flame-level global position.
|
||||
late final Vector2 canvasStartPosition =
|
||||
_game.convertGlobalToLocalCoordinate(deviceStartPosition);
|
||||
|
||||
/// Event start position in the local coordinate space of the current
|
||||
/// component.
|
||||
///
|
||||
/// This property is only accessible when the event is being propagated to
|
||||
/// the components via [deliverAtPoint]. It is an error to try to read this
|
||||
/// property at other times.
|
||||
Vector2 get localStartPosition => renderingTrace.last.start;
|
||||
|
||||
/// Event end position in the coordinate space of the device -- either the
|
||||
/// phone, or the browser window, or the app.
|
||||
///
|
||||
/// If the game runs in a full-screen mode, then this would be equal to the
|
||||
/// [canvasEndPosition]. Otherwise, the [deviceEndPosition] is the
|
||||
/// Flutter-level global position.
|
||||
final Vector2 deviceEndPosition;
|
||||
|
||||
/// Event end position in the coordinate space of the game widget, i.e.
|
||||
/// relative to the game canvas.
|
||||
///
|
||||
/// This could be considered the Flame-level global position.
|
||||
late final Vector2 canvasEndPosition =
|
||||
_game.convertGlobalToLocalCoordinate(deviceEndPosition);
|
||||
|
||||
/// Event end position in the local coordinate space of the current
|
||||
/// component.
|
||||
///
|
||||
/// This property is only accessible when the event is being propagated to
|
||||
/// the components via [deliverAtPoint]. It is an error to try to read this
|
||||
/// property at other times.
|
||||
Vector2 get localEndPosition => renderingTrace.last.end;
|
||||
|
||||
/// Event delta in the coordinate space of the device -- either the
|
||||
/// phone, or the browser window, or the app.
|
||||
///
|
||||
/// If the game runs in a full-screen mode, then this would be equal to the
|
||||
/// [canvasDelta]. Otherwise, the [deviceDelta] is the
|
||||
/// Flutter-level global position.
|
||||
late final Vector2 deviceDelta = deviceEndPosition - deviceStartPosition;
|
||||
|
||||
/// Event delta in the coordinate space of the game widget, i.e.
|
||||
/// relative to the game canvas.
|
||||
///
|
||||
/// This could be considered the Flame-level global position.
|
||||
late final Vector2 canvasDelta = canvasEndPosition - canvasStartPosition;
|
||||
|
||||
/// Event delta in the local coordinate space of the current component.
|
||||
///
|
||||
/// This property is only accessible when the event is being propagated to
|
||||
/// the components via [deliverAtPoint]. It is an error to try to read this
|
||||
/// property at other times.
|
||||
Vector2 get localDelta => localEndPosition - localStartPosition;
|
||||
|
||||
@override
|
||||
Iterable<Component> collectApplicableChildren({
|
||||
required Component rootComponent,
|
||||
}) {
|
||||
return rootComponent.componentsAtLocation(
|
||||
(
|
||||
start: canvasStartPosition,
|
||||
end: canvasEndPosition,
|
||||
),
|
||||
renderingTrace,
|
||||
(transform, context) {
|
||||
final start = context.start;
|
||||
final transformedStart = transform.parentToLocal(start);
|
||||
|
||||
final end = context.end;
|
||||
final transformedEnd = transform.parentToLocal(end);
|
||||
|
||||
if (transformedStart == null || transformedEnd == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
start: transformedStart,
|
||||
end: transformedEnd,
|
||||
);
|
||||
},
|
||||
// we only trigger the drag start if the component check passes, but
|
||||
// as the user drags the cursor it could end up outside the component
|
||||
// bounds; we don't want the event to be lost, so we bypass the check.
|
||||
(component, context) => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,25 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/src/events/messages/position_event.dart';
|
||||
import 'package:flame/src/events/messages/displacement_event.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
class DragUpdateEvent extends PositionEvent {
|
||||
class DragUpdateEvent extends DisplacementEvent {
|
||||
DragUpdateEvent(this.pointerId, super.game, DragUpdateDetails details)
|
||||
: timestamp = details.sourceTimeStamp ?? Duration.zero,
|
||||
delta = details.delta.toVector2(),
|
||||
super(
|
||||
devicePosition: details.globalPosition.toVector2(),
|
||||
deviceStartPosition: details.globalPosition.toVector2(),
|
||||
deviceEndPosition:
|
||||
details.globalPosition.toVector2() + details.delta.toVector2(),
|
||||
);
|
||||
|
||||
final int pointerId;
|
||||
final Duration timestamp;
|
||||
final Vector2 delta;
|
||||
|
||||
static final _nanPoint = Vector2.all(double.nan);
|
||||
|
||||
@override
|
||||
Vector2 get localPosition {
|
||||
return renderingTrace.isEmpty ? _nanPoint : renderingTrace.last;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'DragUpdateEvent(devicePosition: $devicePosition, '
|
||||
'canvasPosition: $canvasPosition, '
|
||||
'delta: $delta, '
|
||||
'pointerId: $pointerId, timestamp: $timestamp)';
|
||||
String toString() => 'DragUpdateEvent('
|
||||
'devicePosition: $deviceStartPosition, '
|
||||
'canvasPosition: $canvasStartPosition, '
|
||||
'delta: $localDelta, '
|
||||
'pointerId: $pointerId, '
|
||||
'timestamp: $timestamp'
|
||||
')';
|
||||
}
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/events/messages/event.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// A base event that includes a location context, i.e. a position or set of
|
||||
/// positions in which the event happens.
|
||||
abstract class LocationContextEvent<C> extends Event {
|
||||
/// The stacktrace of coordinates of the event within the components in their
|
||||
/// rendering order.
|
||||
///
|
||||
/// This represents the stack of transformed versions of the same location
|
||||
/// context -- which represents the event point -- but in the coordinate space
|
||||
/// of each parent component until the root.
|
||||
final List<C> renderingTrace = [];
|
||||
|
||||
/// The context in the parent's coordinate space, containing start and end
|
||||
/// points.
|
||||
C? get parentContext {
|
||||
if (renderingTrace.length >= 2) {
|
||||
final lastIndex = renderingTrace.length - 1;
|
||||
return renderingTrace[lastIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@internal
|
||||
Iterable<Component> collectApplicableChildren({
|
||||
required Component rootComponent,
|
||||
});
|
||||
|
||||
@internal
|
||||
void deliverAtPoint<T extends Component>({
|
||||
required Component rootComponent,
|
||||
required void Function(T component) eventHandler,
|
||||
bool deliverToAll = false,
|
||||
}) {
|
||||
final children = collectApplicableChildren(
|
||||
rootComponent: rootComponent,
|
||||
);
|
||||
for (final child in children.whereType<T>()) {
|
||||
continuePropagation = deliverToAll;
|
||||
eventHandler(child);
|
||||
if (!continuePropagation) {
|
||||
CameraComponent.currentCameras.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,16 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/events/messages/event.dart';
|
||||
import 'package:flame/src/events/messages/location_context_event.dart';
|
||||
import 'package:flame/src/game/game.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Base class for events that originate at some point on the screen. These
|
||||
/// include: tap events, drag events, scale events, etc.
|
||||
/// include: tap events, scale events, etc.
|
||||
///
|
||||
/// This class includes properties that describe the position where the event
|
||||
/// has occurred.
|
||||
abstract class PositionEvent extends Event {
|
||||
abstract class PositionEvent extends LocationContextEvent<Vector2> {
|
||||
PositionEvent(this._game, {required this.devicePosition});
|
||||
final Game _game;
|
||||
|
||||
/// Event position in the coordinate space of the game widget, i.e. relative
|
||||
/// to the game canvas.
|
||||
///
|
||||
/// This could be considered the Flame-level global position.
|
||||
late final Vector2 canvasPosition =
|
||||
_game.convertGlobalToLocalCoordinate(devicePosition);
|
||||
|
||||
/// Event position in the coordinate space of the device -- either the phone,
|
||||
/// or the browser window, or the app.
|
||||
///
|
||||
@ -27,6 +19,13 @@ abstract class PositionEvent extends Event {
|
||||
/// global position.
|
||||
final Vector2 devicePosition;
|
||||
|
||||
/// Event position in the coordinate space of the game widget, i.e. relative
|
||||
/// to the game canvas.
|
||||
///
|
||||
/// This could be considered the Flame-level global position.
|
||||
late final Vector2 canvasPosition =
|
||||
_game.convertGlobalToLocalCoordinate(devicePosition);
|
||||
|
||||
/// Event position in the local coordinate space of the current component.
|
||||
///
|
||||
/// This property is only accessible when the event is being propagated to
|
||||
@ -34,35 +33,10 @@ abstract class PositionEvent extends Event {
|
||||
/// property at other times.
|
||||
Vector2 get localPosition => renderingTrace.last;
|
||||
|
||||
/// The stacktrace of coordinates of the event within the components in their
|
||||
/// rendering order.
|
||||
final List<Vector2> renderingTrace = [];
|
||||
|
||||
Vector2? get parentPosition {
|
||||
if (renderingTrace.length >= 2) {
|
||||
return renderingTrace[renderingTrace.length - 2];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the event to components of type <T> that are currently rendered at
|
||||
/// the [canvasPosition].
|
||||
@internal
|
||||
void deliverAtPoint<T extends Component>({
|
||||
@override
|
||||
Iterable<Component> collectApplicableChildren({
|
||||
required Component rootComponent,
|
||||
required void Function(T component) eventHandler,
|
||||
bool deliverToAll = false,
|
||||
}) {
|
||||
for (final child in rootComponent
|
||||
.componentsAtPoint(canvasPosition, renderingTrace)
|
||||
.whereType<T>()) {
|
||||
continuePropagation = deliverToAll;
|
||||
eventHandler(child);
|
||||
if (!continuePropagation) {
|
||||
CameraComponent.currentCameras.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rootComponent.componentsAtPoint(canvasPosition, renderingTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flame/camera.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -112,42 +113,48 @@ void main() {
|
||||
size: Vector2(50, 50),
|
||||
);
|
||||
|
||||
testWithFlameGame('hit testing', (game) async {
|
||||
final world = _MyWorld();
|
||||
final viewport = CircularViewport.ellipse(80, 20)
|
||||
..position = Vector2(20, 30);
|
||||
final camera = CameraComponent(
|
||||
world: world,
|
||||
viewport: viewport,
|
||||
);
|
||||
game.addAll([world, camera]);
|
||||
await game.ready();
|
||||
testWithGame(
|
||||
'hit testing',
|
||||
() => FlameGame(
|
||||
camera: CameraComponent(
|
||||
viewport: CircularViewport.ellipse(80, 20)
|
||||
..position = Vector2(20, 30),
|
||||
world: _MyWorld(),
|
||||
),
|
||||
),
|
||||
(game) async {
|
||||
await game.ready();
|
||||
final viewport = game.camera.viewport;
|
||||
|
||||
bool hit(double x, double y) {
|
||||
final components = game.componentsAtPoint(Vector2(x, y)).toList();
|
||||
return components.first == viewport && components[1] == world;
|
||||
}
|
||||
|
||||
expect(hit(10, 20), false);
|
||||
expect(hit(20, 30), false);
|
||||
expect(hit(25, 35), false);
|
||||
expect(hit(100, 35), true);
|
||||
expect(hit(100, 50), true);
|
||||
expect(hit(180, 50), true);
|
||||
expect(hit(180, 49), false);
|
||||
expect(hit(180, 51), false);
|
||||
|
||||
final nestedPoints = <Vector2>[];
|
||||
final center = Vector2(100, 50);
|
||||
for (final component in game.componentsAtPoint(center, nestedPoints)) {
|
||||
if (component == viewport) {
|
||||
continue;
|
||||
bool hit(double x, double y) {
|
||||
final components = game.componentsAtPoint(Vector2(x, y)).toList();
|
||||
return components.first == viewport && components[1] == game.world;
|
||||
}
|
||||
expect(component, world);
|
||||
expect(nestedPoints.last, Vector2.zero());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hit(10, 20), false);
|
||||
expect(hit(20, 30), false);
|
||||
expect(hit(25, 35), false);
|
||||
expect(hit(100, 35), true);
|
||||
expect(hit(100, 50), true);
|
||||
expect(hit(180, 50), true);
|
||||
expect(hit(180, 49), false);
|
||||
expect(hit(180, 51), false);
|
||||
|
||||
final nestedPoints = <Vector2>[];
|
||||
final center = Vector2(100, 50);
|
||||
for (final component in game.componentsAtPoint(
|
||||
center,
|
||||
nestedPoints,
|
||||
)) {
|
||||
if (component == viewport) {
|
||||
continue;
|
||||
}
|
||||
expect(component, game.world);
|
||||
expect(nestedPoints.last, Vector2.zero());
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('set wrong size', () {
|
||||
expect(
|
||||
|
||||
@ -443,34 +443,37 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWithFlameGame('componentsAtPoint for transparent route', (game) async {
|
||||
final initialComponent = PositionComponent(size: Vector2.all(100));
|
||||
final newComponent = PositionComponent(size: Vector2.all(100));
|
||||
final router = RouterComponent(
|
||||
initialRoute: 'initial',
|
||||
routes: {
|
||||
'initial': Route(
|
||||
() => initialComponent,
|
||||
),
|
||||
'new': Route(
|
||||
() => newComponent,
|
||||
transparent: true,
|
||||
),
|
||||
},
|
||||
)..addToParent(game);
|
||||
await game.ready();
|
||||
testWithFlameGame(
|
||||
'componentsAtPoint for transparent route',
|
||||
(game) async {
|
||||
final initialComponent = PositionComponent(size: Vector2.all(100));
|
||||
final newComponent = PositionComponent(size: Vector2.all(100));
|
||||
final router = RouterComponent(
|
||||
initialRoute: 'initial',
|
||||
routes: {
|
||||
'initial': Route(
|
||||
() => initialComponent,
|
||||
),
|
||||
'new': Route(
|
||||
() => newComponent,
|
||||
transparent: true,
|
||||
),
|
||||
},
|
||||
)..addToParent(game);
|
||||
await game.ready();
|
||||
|
||||
router.pushNamed('new');
|
||||
await game.ready();
|
||||
expect(
|
||||
game.componentsAtPoint(Vector2(50, 50)).contains(newComponent),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
router.pushNamed('new');
|
||||
await game.ready();
|
||||
expect(
|
||||
game.componentsAtPoint(Vector2(50, 50)).contains(newComponent),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/multi_drag_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@ -211,7 +211,7 @@ void main() {
|
||||
expect(game.children.elementAt(1), isA<_DragWithCallbacksComponent>());
|
||||
expect(game.children.elementAt(2), isA<MultiDragDispatcher>());
|
||||
|
||||
// regular drag
|
||||
// regular drag
|
||||
await tester.timedDragFrom(
|
||||
const Offset(50, 50),
|
||||
const Offset(20, 0),
|
||||
@ -221,7 +221,7 @@ void main() {
|
||||
expect(nDragUpdateCalled, 8);
|
||||
expect(nDragEndCalled, 1);
|
||||
|
||||
// cancelled drag
|
||||
// cancelled drag
|
||||
final gesture = await tester.startGesture(const Offset(50, 50));
|
||||
await gesture.moveBy(const Offset(10, 10));
|
||||
await gesture.cancel();
|
||||
@ -262,7 +262,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'drag event can move outside the component bounds',
|
||||
'drag event can move outside the component bounds and still fire',
|
||||
(tester) async {
|
||||
final points = <Vector2>[];
|
||||
final game = FlameGame(
|
||||
@ -270,7 +270,7 @@ void main() {
|
||||
_DragWithCallbacksComponent(
|
||||
size: Vector2.all(95),
|
||||
position: Vector2.all(5),
|
||||
onDragUpdate: (e) => points.add(e.localPosition),
|
||||
onDragUpdate: (e) => points.add(e.localStartPosition),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -289,16 +289,110 @@ void main() {
|
||||
expect(points.length, 42);
|
||||
expect(points.first, Vector2(75, 75));
|
||||
expect(
|
||||
points.skip(1).take(20),
|
||||
List.generate(20, (i) => Vector2(75.0, 75.0 + i)),
|
||||
);
|
||||
expect(
|
||||
points.skip(21),
|
||||
everyElement(predicate((Vector2 v) => v.isNaN)),
|
||||
points.skip(1),
|
||||
List.generate(41, (i) => Vector2(75.0, 75.0 + i)),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'drag event delta respects camera & zoom',
|
||||
(tester) async {
|
||||
// canvas size is 800x600 so this means a 10x logical scale across
|
||||
// both dimensions
|
||||
final resolution = Vector2(80, 60);
|
||||
final game = FlameGame(
|
||||
camera: CameraComponent.withFixedResolution(
|
||||
width: resolution.x,
|
||||
height: resolution.y,
|
||||
),
|
||||
);
|
||||
|
||||
game.camera.viewfinder.zoom = 2;
|
||||
|
||||
final deltas = <Vector2>[];
|
||||
await game.world.add(
|
||||
_DragWithCallbacksComponent(
|
||||
position: Vector2.all(-5),
|
||||
size: Vector2.all(10),
|
||||
onDragUpdate: (event) => deltas.add(event.localDelta),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(GameWidget(game: game));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final canvasSize = game.canvasSize;
|
||||
await tester.dragFrom(
|
||||
(canvasSize / 2).toOffset(),
|
||||
Offset(canvasSize.x / 10, 0),
|
||||
);
|
||||
final totalDelta = deltas.reduce((a, b) => a + b);
|
||||
expect(totalDelta, Vector2(4, 0));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'drag event delta respects widget positioning',
|
||||
(tester) async {
|
||||
// canvas size is 800x600 so this means a 10x logical scale across
|
||||
// both dimensions
|
||||
final resolution = Vector2(80, 60);
|
||||
final game = FlameGame(
|
||||
camera: CameraComponent.withFixedResolution(
|
||||
width: resolution.x,
|
||||
height: resolution.y,
|
||||
),
|
||||
);
|
||||
|
||||
game.camera.viewfinder.zoom = 1 / 2;
|
||||
|
||||
final deltas = <Vector2>[];
|
||||
await game.world.add(
|
||||
_DragWithCallbacksComponent(
|
||||
position: Vector2.all(-5),
|
||||
size: Vector2.all(10),
|
||||
onDragUpdate: (event) => deltas.add(event.localDelta),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 100.0,
|
||||
top: 200.0,
|
||||
width: 800,
|
||||
height: 600,
|
||||
child: GameWidget(game: game),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final canvasSize = game.canvasSize;
|
||||
|
||||
// no offset
|
||||
await tester.dragFrom(
|
||||
(canvasSize / 2).toOffset(),
|
||||
Offset(canvasSize.x / 10, 0),
|
||||
);
|
||||
expect(deltas, isEmpty);
|
||||
|
||||
// accounting for offset
|
||||
await tester.dragFrom(
|
||||
(canvasSize / 2 + Vector2(100, 200)).toOffset(),
|
||||
Offset(canvasSize.x / 10, 0),
|
||||
);
|
||||
expect(deltas, isNotEmpty);
|
||||
final totalDelta = deltas.reduce((a, b) => a + b);
|
||||
expect(totalDelta, Vector2(16, 0));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
mixin _DragCounter on DragCallbacks {
|
||||
|
||||
Reference in New Issue
Block a user