mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 00:17:20 +08:00
feat: Add HoverCallbacks (#2706)
This creates HoverCallbacks (and PointerMoveCallbacks) to replicate the Hoverables behaviour in the new camera and event system.
This commit is contained in:
1
.github/.cspell/gamedev_dictionary.txt
vendored
1
.github/.cspell/gamedev_dictionary.txt
vendored
@ -156,3 +156,4 @@ viewports
|
||||
vsync
|
||||
widget's
|
||||
unawaited
|
||||
proxied
|
||||
@ -18,6 +18,7 @@ import 'package:doc_flame_examples/move_to_effect.dart';
|
||||
import 'package:doc_flame_examples/opacity_by_effect.dart';
|
||||
import 'package:doc_flame_examples/opacity_effect_with_target.dart';
|
||||
import 'package:doc_flame_examples/opacity_to_effect.dart';
|
||||
import 'package:doc_flame_examples/pointer_events.dart';
|
||||
import 'package:doc_flame_examples/ray_cast.dart';
|
||||
import 'package:doc_flame_examples/ray_trace.dart';
|
||||
import 'package:doc_flame_examples/remove_effect.dart';
|
||||
@ -60,6 +61,7 @@ void main() {
|
||||
'opacity_by_effect': OpacityByEffectGame.new,
|
||||
'opacity_effect_with_target': OpacityEffectWithTargetGame.new,
|
||||
'opacity_to_effect': OpacityToEffectGame.new,
|
||||
'pointer_events': PointerEventsGame.new,
|
||||
'ray_cast': RayCastExample.new,
|
||||
'ray_trace': RayTraceExample.new,
|
||||
'remove_effect': RemoveEffectGame.new,
|
||||
|
||||
51
doc/flame/examples/lib/pointer_events.dart
Normal file
51
doc/flame/examples/lib/pointer_events.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class PointerEventsGame extends FlameGame with TapCallbacks {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(HoverTarget(Vector2(100, 200)));
|
||||
add(HoverTarget(Vector2(300, 300)));
|
||||
add(HoverTarget(Vector2(400, 50)));
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapDown(TapDownEvent event) {
|
||||
add(HoverTarget(event.localPosition));
|
||||
}
|
||||
}
|
||||
|
||||
class HoverTarget extends PositionComponent with HoverCallbacks {
|
||||
static final Random _random = Random();
|
||||
|
||||
HoverTarget(Vector2 position)
|
||||
: super(
|
||||
position: position,
|
||||
size: Vector2.all(50),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
final _paint = Paint()
|
||||
..color = HSLColor.fromAHSL(1, _random.nextDouble() * 360, 1, 0.8)
|
||||
.toColor()
|
||||
.withOpacity(0.5);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.drawRect(size.toRect(), _paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverEnter() {
|
||||
_paint.color = _paint.color.withOpacity(1);
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverExit() {
|
||||
_paint.color = _paint.color.withOpacity(0.5);
|
||||
}
|
||||
}
|
||||
84
doc/flame/inputs/pointer_events.md
Normal file
84
doc/flame/inputs/pointer_events.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Pointer Events
|
||||
|
||||
```{note}
|
||||
This document describes the new events API. The old (legacy) approach,
|
||||
which is still supported, is described in [](gesture_input.md).
|
||||
```
|
||||
|
||||
**Pointer events** are Flutter's generalized "mouse-movement"-type events (for desktop or web).
|
||||
|
||||
If you want to interact with mouse movement events within your component or game, you can use the
|
||||
`PointerMoveCallbacks` mixin.
|
||||
|
||||
For example:
|
||||
|
||||
```dart
|
||||
class MyComponent extends PositionComponent with PointerMoveCallbacks {
|
||||
MyComponent() : super(size: Vector2(80, 60));
|
||||
|
||||
@override
|
||||
void onPointerMove(PointerMoveEvent event) {
|
||||
// Do something in response to the mouse move (e.g. update coordinates)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The mixin adds two overridable methods to your component:
|
||||
|
||||
- `onPointerMove`: called when the mouse moves within the component
|
||||
- `onPointerMoveStop`: called once if the component was being hovered and the mouse leaves
|
||||
|
||||
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 in
|
||||
`PositionComponent`, 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.
|
||||
|
||||
Note that only mouse events happening within your component will be proxied along. However,
|
||||
`onPointerMoveStop` will be fired once on the first mouse movement that leaves your component, so
|
||||
you can handle any exit conditions there.
|
||||
|
||||
|
||||
## HoverCallbacks
|
||||
|
||||
If you want to specifically know if your component is being hovered or not, or if you want to hook
|
||||
into hover enter and exist events, you can use a more dedicated mixin called `HoverCallbacks`.
|
||||
|
||||
For example:
|
||||
|
||||
```dart
|
||||
class MyComponent extends PositionComponent with HoverCallbacks {
|
||||
|
||||
MyComponent() : super(size: Vector2(80, 60));
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
// use `isHovered` to know if the component is being hovered
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverEnter() {
|
||||
// Do something in response to the mouse entering the component
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverExit() {
|
||||
// Do something in response to the mouse leaving the component
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that you can still listen to the "raw" onPointerMove methods for additional functionality, just
|
||||
make sure to call the `super` version to enable the `HoverCallbacks` behavior.
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
Play with the demo below to see the pointer hover events in action.
|
||||
|
||||
```{flutter-app}
|
||||
:sources: ../flame/examples
|
||||
:page: pointer_events
|
||||
:show: widget code
|
||||
```
|
||||
@ -1,7 +1,7 @@
|
||||
# Tap Events
|
||||
|
||||
```{note}
|
||||
This document describes the new tap events API. The old (legacy) approach,
|
||||
This document describes the new events API. The old (legacy) approach,
|
||||
which is still supported, is described in [](gesture_input.md).
|
||||
```
|
||||
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HoverablesExample extends FlameGame with HasHoverables, TapDetector {
|
||||
class HoverCallbacksExample extends FlameGame with TapCallbacks {
|
||||
static const String description = '''
|
||||
This example shows how to use `Hoverable`s.\n\n
|
||||
This example shows how to use `HoverCallbacks`s.\n\n
|
||||
Add more squares by clicking and hover them to change their color.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(HoverableSquare(Vector2(200, 500)));
|
||||
add(HoverableSquare(Vector2(700, 300)));
|
||||
add(HoverSquare(Vector2(200, 500)));
|
||||
add(HoverSquare(Vector2(700, 300)));
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapDown(TapDownInfo info) {
|
||||
add(HoverableSquare(info.eventPosition.game));
|
||||
void onTapDown(TapDownEvent event) {
|
||||
add(HoverSquare(event.localPosition));
|
||||
}
|
||||
}
|
||||
|
||||
class HoverableSquare extends PositionComponent with Hoverable {
|
||||
class HoverSquare extends PositionComponent with HoverCallbacks {
|
||||
static final Paint _white = Paint()..color = const Color(0xFFFFFFFF);
|
||||
static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5);
|
||||
|
||||
HoverableSquare(Vector2 position)
|
||||
: super(position: position, size: Vector2.all(100)) {
|
||||
anchor = Anchor.center;
|
||||
}
|
||||
HoverSquare(Vector2 position)
|
||||
: super(
|
||||
position: position,
|
||||
size: Vector2.all(100),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
@ -4,7 +4,7 @@ import 'package:examples/stories/input/double_tap_callbacks_example.dart';
|
||||
import 'package:examples/stories/input/draggables_example.dart';
|
||||
import 'package:examples/stories/input/gesture_hitboxes_example.dart';
|
||||
import 'package:examples/stories/input/hardware_keyboard_example.dart';
|
||||
import 'package:examples/stories/input/hoverables_example.dart';
|
||||
import 'package:examples/stories/input/hover_callbacks_example.dart';
|
||||
import 'package:examples/stories/input/joystick_advanced_example.dart';
|
||||
import 'package:examples/stories/input/joystick_example.dart';
|
||||
import 'package:examples/stories/input/keyboard_example.dart';
|
||||
@ -50,10 +50,10 @@ void addInputStories(Dashbook dashbook) {
|
||||
info: DoubleTapCallbacksExample.description,
|
||||
)
|
||||
..add(
|
||||
'Hoverables',
|
||||
(_) => GameWidget(game: HoverablesExample()),
|
||||
codeLink: baseLink('input/hoverables_example.dart'),
|
||||
info: HoverablesExample.description,
|
||||
'HoverCallbacks',
|
||||
(_) => GameWidget(game: HoverCallbacksExample()),
|
||||
codeLink: baseLink('input/hover_callbacks_example.dart'),
|
||||
info: HoverCallbacksExample.description,
|
||||
)
|
||||
..add(
|
||||
'Keyboard',
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
export 'src/events/component_mixins/double_tap_callbacks.dart'
|
||||
show DoubleTapCallbacks;
|
||||
export 'src/events/component_mixins/drag_callbacks.dart' show DragCallbacks;
|
||||
export 'src/events/component_mixins/hover_callbacks.dart' show HoverCallbacks;
|
||||
export 'src/events/component_mixins/pointer_move_callbacks.dart'
|
||||
show PointerMoveCallbacks;
|
||||
export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks;
|
||||
export 'src/events/flame_game_mixins/has_draggables_bridge.dart'
|
||||
show HasDraggablesBridge;
|
||||
@ -22,6 +25,7 @@ export 'src/events/messages/drag_cancel_event.dart' show DragCancelEvent;
|
||||
export 'src/events/messages/drag_end_event.dart' show DragEndEvent;
|
||||
export 'src/events/messages/drag_start_event.dart' show DragStartEvent;
|
||||
export 'src/events/messages/drag_update_event.dart' show DragUpdateEvent;
|
||||
export 'src/events/messages/pointer_move_event.dart' show PointerMoveEvent;
|
||||
export 'src/events/messages/tap_cancel_event.dart' show TapCancelEvent;
|
||||
export 'src/events/messages/tap_down_event.dart' show TapDownEvent;
|
||||
export 'src/events/messages/tap_up_event.dart' show TapUpEvent;
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// This mixin can be added to a [Component] allowing it to receive hover
|
||||
/// events.
|
||||
///
|
||||
/// In addition to adding this mixin, the component must also implement the
|
||||
/// [containsLocalPoint] method -- the component will only be considered
|
||||
/// "hovered" if the point where the hover event occurred is inside the
|
||||
/// component.
|
||||
///
|
||||
/// This mixin is the replacement of the Hoverable mixin.
|
||||
mixin HoverCallbacks on Component implements PointerMoveCallbacks {
|
||||
bool _isHovered = false;
|
||||
|
||||
/// Returns true while the component is being dragged.
|
||||
bool get isHovered => _isHovered;
|
||||
|
||||
void onHoverEnter() {}
|
||||
|
||||
void onHoverExit() {}
|
||||
|
||||
void _doHoverEnter() {
|
||||
_isHovered = true;
|
||||
onHoverEnter();
|
||||
}
|
||||
|
||||
void _doHoverExit() {
|
||||
_isHovered = false;
|
||||
onHoverExit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPointerMove(PointerMoveEvent event) {
|
||||
final position = event.localPosition;
|
||||
if (containsLocalPoint(position)) {
|
||||
if (!_isHovered) {
|
||||
_doHoverEnter();
|
||||
}
|
||||
} else {
|
||||
if (_isHovered) {
|
||||
_doHoverExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onPointerMoveStop(PointerMoveEvent event) {
|
||||
if (_isHovered) {
|
||||
_doHoverExit();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
PointerMoveCallbacks.onMountHandler(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// This mixin can be added to a [Component] allowing it to receive
|
||||
/// pointer movement events.
|
||||
mixin PointerMoveCallbacks on Component {
|
||||
void onPointerMove(PointerMoveEvent event) {}
|
||||
|
||||
void onPointerMoveStop(PointerMoveEvent event) {}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
onMountHandler(this);
|
||||
}
|
||||
|
||||
static void onMountHandler(PointerMoveCallbacks instance) {
|
||||
final game = instance.findGame()!;
|
||||
const key = MouseMoveDispatcherKey();
|
||||
if (game.findByKey(key) == null) {
|
||||
final dispatcher = PointerMoveDispatcher();
|
||||
game.registerKey(key, dispatcher);
|
||||
game.add(dispatcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:flame/src/components/core/component_key.dart';
|
||||
import 'package:flame/src/events/tagged_component.dart';
|
||||
import 'package:flame/src/game/flame_game.dart';
|
||||
import 'package:flutter/gestures.dart' as flutter;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// **MouseMoveDispatcher** facilitates dispatching of mouse move events to the
|
||||
/// [PointerMoveCallbacks] components in the component tree. It will be attached
|
||||
/// to the [FlameGame] instance automatically whenever any
|
||||
/// [PointerMoveCallbacks] components are mounted into the component tree.
|
||||
@internal
|
||||
class PointerMoveDispatcher extends Component {
|
||||
/// The record of all components currently being hovered.
|
||||
final Set<TaggedComponent<PointerMoveCallbacks>> _records = {};
|
||||
|
||||
FlameGame get game => parent! as FlameGame;
|
||||
|
||||
@mustCallSuper
|
||||
void onMouseMove(PointerMoveEvent event) {
|
||||
final updated = <TaggedComponent<PointerMoveCallbacks>>{};
|
||||
|
||||
event.deliverAtPoint(
|
||||
rootComponent: game,
|
||||
deliverToAll: true,
|
||||
eventHandler: (PointerMoveCallbacks component) {
|
||||
final tagged = TaggedComponent(event.pointerId, component);
|
||||
_records.add(tagged);
|
||||
updated.add(tagged);
|
||||
component.onPointerMove(event);
|
||||
},
|
||||
);
|
||||
|
||||
final toRemove = <TaggedComponent<PointerMoveCallbacks>>{};
|
||||
for (final record in _records) {
|
||||
if (record.pointerId == event.pointerId && !updated.contains(record)) {
|
||||
// one last "exit" event
|
||||
record.component.onPointerMoveStop(event);
|
||||
toRemove.add(record);
|
||||
}
|
||||
}
|
||||
_records.removeAll(toRemove);
|
||||
}
|
||||
|
||||
void _handlePointerMove(flutter.PointerHoverEvent event) {
|
||||
onMouseMove(PointerMoveEvent.fromPointerHoverEvent(game, event));
|
||||
}
|
||||
|
||||
@override
|
||||
void onMount() {
|
||||
game.mouseDetector = _handlePointerMove;
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
game.mouseDetector = null;
|
||||
game.unregisterKey(const MouseMoveDispatcherKey());
|
||||
}
|
||||
}
|
||||
|
||||
class MouseMoveDispatcherKey implements ComponentKey {
|
||||
const MouseMoveDispatcherKey();
|
||||
|
||||
@override
|
||||
int get hashCode => 'MouseMoveDispatcherKey'.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
other is MouseMoveDispatcherKey && other.hashCode == hashCode;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/messages/position_event.dart';
|
||||
import 'package:flutter/gestures.dart' as flutter;
|
||||
|
||||
class PointerMoveEvent extends PositionEvent {
|
||||
PointerMoveEvent(
|
||||
this.pointerId,
|
||||
super.game,
|
||||
flutter.PointerHoverEvent rawEvent,
|
||||
) : timestamp = rawEvent.timeStamp,
|
||||
delta = rawEvent.delta.toVector2(),
|
||||
super(
|
||||
devicePosition: rawEvent.position.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() => 'PointerMoveEvent(devicePosition: $devicePosition, '
|
||||
'canvasPosition: $canvasPosition, '
|
||||
'delta: $delta, '
|
||||
'pointerId: $pointerId, timestamp: $timestamp)';
|
||||
|
||||
factory PointerMoveEvent.fromPointerHoverEvent(
|
||||
Game game,
|
||||
flutter.PointerHoverEvent event,
|
||||
) {
|
||||
return PointerMoveEvent(
|
||||
event.pointer,
|
||||
game,
|
||||
event,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import 'package:flame/src/game/game_render_box.dart';
|
||||
import 'package:flame/src/game/game_widget/gesture_detector_builder.dart';
|
||||
import 'package:flame/src/game/overlay_manager.dart';
|
||||
import 'package:flame/src/game/projector.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
@ -35,6 +36,10 @@ abstract mixin class Game {
|
||||
late final GestureDetectorBuilder gestureDetectors =
|
||||
GestureDetectorBuilder(refreshWidget)..initializeGestures(this);
|
||||
|
||||
/// Set by the PointerMoveDispatcher to receive mouse events from the
|
||||
/// game widget.
|
||||
void Function(PointerHoverEvent event)? mouseDetector;
|
||||
|
||||
/// This should update the state of the game.
|
||||
void update(double dt);
|
||||
|
||||
|
||||
@ -174,7 +174,8 @@ bool hasMouseDetectors(Game game) {
|
||||
return game is MouseMovementDetector ||
|
||||
game is ScrollDetector ||
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
game is HasHoverables;
|
||||
game is HasHoverables ||
|
||||
game.mouseDetector != null;
|
||||
}
|
||||
|
||||
Widget applyMouseDetectors(Game game, Widget child) {
|
||||
@ -182,10 +183,14 @@ Widget applyMouseDetectors(Game game, Widget child) {
|
||||
? game.onMouseMove
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
: (game is HasHoverables ? game.onMouseMove : null);
|
||||
final mouseDetector = game.mouseDetector;
|
||||
return Listener(
|
||||
child: MouseRegion(
|
||||
child: child,
|
||||
onHover: (e) => mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e)),
|
||||
onHover: (PointerHoverEvent e) {
|
||||
mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e));
|
||||
mouseDetector?.call(e);
|
||||
},
|
||||
),
|
||||
onPointerSignal: (event) =>
|
||||
game is ScrollDetector && event is PointerScrollEvent
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('HoverCallbacks', () {
|
||||
testWithFlameGame(
|
||||
'make sure HoverCallbacks components can be added to a FlameGame',
|
||||
(game) async {
|
||||
await game.ensureAdd(_HoverCallbacksComponent());
|
||||
await game.ready();
|
||||
|
||||
_hasDispatcher(game);
|
||||
});
|
||||
|
||||
testWithFlameGame('receive hover events', (game) async {
|
||||
final component = _HoverCallbacksComponent(
|
||||
position: Vector2.all(10),
|
||||
size: Vector2.all(10),
|
||||
);
|
||||
game.add(component);
|
||||
await game.ready();
|
||||
|
||||
_hasDispatcher(game);
|
||||
|
||||
_mouseEvent(game, Vector2.all(12));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 0);
|
||||
|
||||
_mouseEvent(game, Vector2.all(14));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 0);
|
||||
|
||||
_mouseEvent(game, Vector2.all(16));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 0);
|
||||
|
||||
_mouseEvent(game, Vector2.all(18));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 0);
|
||||
|
||||
_mouseEvent(game, Vector2.all(20));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 1);
|
||||
|
||||
_mouseEvent(game, Vector2.all(22));
|
||||
component.checkHoverEventCounts(enter: 1, exit: 1);
|
||||
|
||||
_mouseEvent(game, Vector2.all(18));
|
||||
component.checkHoverEventCounts(enter: 2, exit: 1);
|
||||
|
||||
_mouseEvent(game, Vector2.all(19));
|
||||
component.checkHoverEventCounts(enter: 2, exit: 1);
|
||||
|
||||
_mouseEvent(game, Vector2.all(20));
|
||||
component.checkHoverEventCounts(enter: 2, exit: 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _mouseEvent(FlameGame game, Vector2 position) {
|
||||
game.firstChild<PointerMoveDispatcher>()!.onMouseMove(
|
||||
createMouseMoveEvent(
|
||||
game: game,
|
||||
position: position,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _hasDispatcher(FlameGame game) {
|
||||
expect(
|
||||
game.children.whereType<PointerMoveDispatcher>(),
|
||||
hasLength(1),
|
||||
);
|
||||
}
|
||||
|
||||
mixin _HoverInspector on HoverCallbacks {
|
||||
int hoverEnterEvent = 0;
|
||||
int hoverExitEvent = 0;
|
||||
|
||||
void checkHoverEventCounts({required int enter, required int exit}) {
|
||||
expect(
|
||||
hoverEnterEvent,
|
||||
equals(enter),
|
||||
reason: 'Mismatched hover enter event count',
|
||||
);
|
||||
expect(
|
||||
hoverExitEvent,
|
||||
equals(exit),
|
||||
reason: 'Mismatched hover exit event count',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverEnter() {
|
||||
hoverEnterEvent++;
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverExit() {
|
||||
hoverExitEvent++;
|
||||
}
|
||||
}
|
||||
|
||||
class _HoverCallbacksComponent extends PositionComponent
|
||||
with HoverCallbacks, _HoverInspector {
|
||||
_HoverCallbacksComponent({
|
||||
super.position,
|
||||
super.size,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('PointerMoveCallbacks', () {
|
||||
testWithFlameGame(
|
||||
'make sure PointerMoveCallbacks components can be added to a FlameGame',
|
||||
(game) async {
|
||||
await game.ensureAdd(_PointerMoveCallbacksComponent());
|
||||
await game.ready();
|
||||
|
||||
_hasDispatcher(game);
|
||||
});
|
||||
|
||||
testWithFlameGame('receive pointer move events on component', (game) async {
|
||||
final c1 = _PointerMoveCallbacksComponent(
|
||||
position: Vector2.all(10),
|
||||
size: Vector2.all(10),
|
||||
);
|
||||
game.add(c1);
|
||||
final c2 = _PointerMoveCallbacksComponent(
|
||||
position: Vector2.all(15),
|
||||
size: Vector2.all(10),
|
||||
);
|
||||
game.add(c2);
|
||||
|
||||
await game.ready();
|
||||
|
||||
_hasDispatcher(game);
|
||||
|
||||
_mouseEvent(game, Vector2.all(12));
|
||||
expect(c1.removeSingle(), Vector2.all(2));
|
||||
expect(c2.receivedEventsAt, isEmpty);
|
||||
|
||||
_mouseEvent(game, Vector2.all(1));
|
||||
expect(c1.receivedEventsAt, isEmpty);
|
||||
expect(c2.receivedEventsAt, isEmpty);
|
||||
|
||||
_mouseEvent(game, Vector2.all(19));
|
||||
expect(c1.removeSingle(), Vector2.all(9));
|
||||
expect(c2.removeSingle(), Vector2.all(4));
|
||||
|
||||
_mouseEvent(game, Vector2.all(21));
|
||||
expect(c1.receivedEventsAt, isEmpty);
|
||||
expect(c2.removeSingle(), Vector2.all(6));
|
||||
});
|
||||
|
||||
testWithGame(
|
||||
'receive pointer move events on game',
|
||||
_PointerMoveCallbacksGame.new,
|
||||
(game) async {
|
||||
_hasDispatcher(game);
|
||||
|
||||
_mouseEvent(game, Vector2.all(12));
|
||||
expect(game.removeSingle(), Vector2.all(12));
|
||||
|
||||
_mouseEvent(game, Vector2.all(1));
|
||||
expect(game.removeSingle(), Vector2.all(1));
|
||||
|
||||
_mouseEvent(game, Vector2.all(19));
|
||||
expect(game.removeSingle(), Vector2.all(19));
|
||||
|
||||
_mouseEvent(game, Vector2.all(21));
|
||||
expect(game.removeSingle(), Vector2.all(21));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _mouseEvent(FlameGame game, Vector2 position) {
|
||||
game.firstChild<PointerMoveDispatcher>()!.onMouseMove(
|
||||
createMouseMoveEvent(
|
||||
game: game,
|
||||
position: position,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _hasDispatcher(FlameGame game) {
|
||||
expect(
|
||||
game.children.whereType<PointerMoveDispatcher>(),
|
||||
hasLength(1),
|
||||
);
|
||||
}
|
||||
|
||||
mixin _PointerMoveInspector on PointerMoveCallbacks {
|
||||
List<Vector2> receivedEventsAt = [];
|
||||
|
||||
Vector2 removeSingle() {
|
||||
expect(receivedEventsAt, hasLength(1));
|
||||
return receivedEventsAt.removeAt(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void onPointerMove(PointerMoveEvent event) {
|
||||
receivedEventsAt.add(event.localPosition);
|
||||
}
|
||||
}
|
||||
|
||||
class _PointerMoveCallbacksComponent extends PositionComponent
|
||||
with PointerMoveCallbacks, _PointerMoveInspector {
|
||||
_PointerMoveCallbacksComponent({
|
||||
super.position,
|
||||
super.size,
|
||||
});
|
||||
}
|
||||
|
||||
class _PointerMoveCallbacksGame extends FlameGame
|
||||
with PointerMoveCallbacks, _PointerMoveInspector {}
|
||||
@ -6,6 +6,7 @@ export 'src/fails_assert.dart';
|
||||
export 'src/flame_test.dart';
|
||||
export 'src/mock_gesture_events.dart';
|
||||
export 'src/mock_image.dart';
|
||||
export 'src/mock_pointer_move_event.dart';
|
||||
export 'src/mock_tap_drag_events.dart';
|
||||
export 'src/random_test.dart';
|
||||
export 'src/test_flame_game.dart';
|
||||
|
||||
22
packages/flame_test/lib/src/mock_pointer_move_event.dart
Normal file
22
packages/flame_test/lib/src/mock_pointer_move_event.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/gestures.dart' as flutter;
|
||||
|
||||
PointerMoveEvent createMouseMoveEvent({
|
||||
required Game game,
|
||||
int? pointerId,
|
||||
Vector2? position,
|
||||
Vector2? delta,
|
||||
Duration? timestamp,
|
||||
}) {
|
||||
return PointerMoveEvent(
|
||||
pointerId ?? 1,
|
||||
game,
|
||||
flutter.PointerHoverEvent(
|
||||
timeStamp: timestamp ?? Duration.zero,
|
||||
position: position?.toOffset() ?? Offset.zero,
|
||||
delta: delta?.toOffset() ?? Offset.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user