mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +08:00
Add hoverables (#797)
This commit is contained in:
31
doc/input.md
31
doc/input.md
@ -138,10 +138,10 @@ class MyGame extends Game with TapDetector {
|
||||
You can also check more complete examples
|
||||
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/controls/).
|
||||
|
||||
## Tapable and Draggable components
|
||||
## Tapable, Draggable and Hoverable components
|
||||
|
||||
Any component derived from `BaseComponent` (most components) can add the `Tapable` and/or the
|
||||
`Draggable` mixins to handle taps and drags on the component.
|
||||
Any component derived from `BaseComponent` (most components) can add the `Tapable`, the
|
||||
`Draggable`, and/or the `Hoverable` mixins to handle taps, drags and hovers on the component.
|
||||
|
||||
All overridden methods return a boolean to control if the event should be passed down further along
|
||||
to components underneath it. So say that you only want your top visible component to receive a tap
|
||||
@ -166,8 +166,7 @@ void onTapUp(TapUpInfo event) {}
|
||||
Minimal component example:
|
||||
|
||||
```dart
|
||||
import 'package:flame/components/component.dart';
|
||||
import 'package:flame/components/mixins/tapable.dart';
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class TapableComponent extends PositionComponent with Tapable {
|
||||
|
||||
@ -229,8 +228,7 @@ Minimal component example (this example ignores pointerId so it wont work well i
|
||||
multi-drag):
|
||||
|
||||
```dart
|
||||
import 'package:flame/components/component.dart';
|
||||
import 'package:flame/components/mixins/draggable.dart';
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class DraggableComponent extends PositionComponent with Draggable {
|
||||
|
||||
@ -275,6 +273,25 @@ class MyGame extends BaseGame with HasDraggableComponents {
|
||||
**Note**: `HasDraggableComponents` uses an advanced gesture detector under the hood and as explained
|
||||
further up on this page, shouldn't be used alongside basic detectors.
|
||||
|
||||
### Hoverable components
|
||||
|
||||
Just like the others, this mixin allows for easy wiring of your component to listen to hover states
|
||||
and events.
|
||||
|
||||
By adding the `HasHoverableComponents` mixin to your base game, and by using the mixin `Hoverable` on
|
||||
your components, they get an `isHovered` field and a couple of methods (`onHoverStart`, `onHoverEnd`) that
|
||||
you can override if you want to listen to the events.
|
||||
|
||||
```dart
|
||||
bool isHovered = false;
|
||||
void onHoverEnter(PointerHoverInfo event) {}
|
||||
void onHoverLeave(PointerHoverInfo event) {}
|
||||
```
|
||||
|
||||
The provided event info is from the mouse move that triggered the action (entering or leaving).
|
||||
While the mouse movement is kept inside or outside, no events are fired and those mouse move events are
|
||||
not propagated. Only when the state is changed the handlers are triggered.
|
||||
|
||||
## Hitbox
|
||||
The `Hitbox` mixin is used to make detection of gestures on top of your `PositionComponent`s more
|
||||
accurate. Say that you have a fairly round rock as a `SpriteComponent` for example, then you don't
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flame/game.dart';
|
||||
import '../../commons/commons.dart';
|
||||
import 'advanced_joystick.dart';
|
||||
import 'draggables.dart';
|
||||
import 'hoverables.dart';
|
||||
import 'joystick.dart';
|
||||
import 'keyboard.dart';
|
||||
import 'mouse_movement.dart';
|
||||
@ -55,16 +56,18 @@ void addControlsStories(Dashbook dashbook) {
|
||||
(context) {
|
||||
return GameWidget(
|
||||
game: DraggablesGame(
|
||||
zoom: context.listProperty(
|
||||
'zoom',
|
||||
1,
|
||||
[0.5, 1, 1.5],
|
||||
),
|
||||
zoom: context.listProperty('zoom', 1, [0.5, 1, 1.5]),
|
||||
),
|
||||
);
|
||||
},
|
||||
codeLink: baseLink('controls/draggables.dart'),
|
||||
)
|
||||
..add(
|
||||
'Hoverables',
|
||||
(_) => GameWidget(game: HoverablesGame()),
|
||||
codeLink: baseLink('controls/hoverables.dart'),
|
||||
info: 'Add more squares by clicking. Hover squares to change colors.',
|
||||
)
|
||||
..add(
|
||||
'Joystick',
|
||||
(_) => GameWidget(game: JoystickGame()),
|
||||
|
||||
34
examples/lib/stories/controls/hoverables.dart
Normal file
34
examples/lib/stories/controls/hoverables.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
|
||||
class HoverableSquare extends PositionComponent with Hoverable {
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
canvas.drawRect(size.toRect(), isHovered ? _grey : _white);
|
||||
}
|
||||
}
|
||||
|
||||
class HoverablesGame extends BaseGame with HasHoverableComponents, TapDetector {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(HoverableSquare(Vector2(200, 500)));
|
||||
add(HoverableSquare(Vector2(700, 300)));
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapDown(TapDownInfo event) {
|
||||
add(HoverableSquare(event.eventPosition.game));
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
- No need to send size in ParallaxComponent.fromParallax since Parallax already contains it
|
||||
- Fix Text Rendering not working properly
|
||||
- Add more useful methods to the IsometricTileMap component
|
||||
- Add Hoverables
|
||||
|
||||
## [1.0.0-rc10]
|
||||
- Updated tutorial documentation to indicate use of new version
|
||||
|
||||
@ -8,6 +8,7 @@ export 'src/components/mixins/draggable.dart';
|
||||
export 'src/components/mixins/has_collidables.dart';
|
||||
export 'src/components/mixins/has_game_ref.dart';
|
||||
export 'src/components/mixins/hitbox.dart';
|
||||
export 'src/components/mixins/hoverable.dart';
|
||||
export 'src/components/mixins/single_child_particle.dart';
|
||||
export 'src/components/mixins/tapable.dart';
|
||||
export 'src/components/nine_tile_box_component.dart';
|
||||
|
||||
48
packages/flame/lib/src/components/mixins/hoverable.dart
Normal file
48
packages/flame/lib/src/components/mixins/hoverable.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../../game.dart';
|
||||
import '../../game/base_game.dart';
|
||||
import '../../gestures/events.dart';
|
||||
import '../base_component.dart';
|
||||
|
||||
mixin Hoverable on BaseComponent {
|
||||
bool _isHovered = false;
|
||||
bool get isHovered => _isHovered;
|
||||
void onHoverEnter(PointerHoverInfo event) {}
|
||||
void onHoverLeave(PointerHoverInfo event) {}
|
||||
|
||||
@nonVirtual
|
||||
void doHandleMouseMovement(PointerHoverInfo event, Vector2 p) {
|
||||
if (containsPoint(p)) {
|
||||
if (!_isHovered) {
|
||||
_isHovered = true;
|
||||
onHoverEnter(event);
|
||||
}
|
||||
} else {
|
||||
if (_isHovered) {
|
||||
_isHovered = false;
|
||||
onHoverLeave(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixin HasHoverableComponents on BaseGame {
|
||||
@mustCallSuper
|
||||
void onMouseMove(PointerHoverInfo event) {
|
||||
final p = event.eventPosition.game;
|
||||
bool _mouseMoveHandler(Hoverable c) {
|
||||
c.doHandleMouseMovement(event, p);
|
||||
return true; // always continue
|
||||
}
|
||||
|
||||
for (final c in components.toList().reversed) {
|
||||
if (c is BaseComponent) {
|
||||
c.propagateToChildren<Hoverable>(_mouseMoveHandler);
|
||||
}
|
||||
if (c is Hoverable) {
|
||||
_mouseMoveHandler(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import '../components/mixins/collidable.dart';
|
||||
import '../components/mixins/draggable.dart';
|
||||
import '../components/mixins/has_collidables.dart';
|
||||
import '../components/mixins/has_game_ref.dart';
|
||||
import '../components/mixins/hoverable.dart';
|
||||
import '../components/mixins/tapable.dart';
|
||||
import '../components/position_component.dart';
|
||||
import '../fps_counter.dart';
|
||||
@ -106,6 +107,12 @@ class BaseGame extends Game with FPSCounter {
|
||||
'Draggable Components can only be added to a BaseGame with HasDraggableComponents',
|
||||
);
|
||||
}
|
||||
if (c is Hoverable) {
|
||||
assert(
|
||||
this is HasHoverableComponents,
|
||||
'Hoverable Components can only be added to a BaseGame with HasHoverableComponents',
|
||||
);
|
||||
}
|
||||
|
||||
if (debugMode && c is PositionComponent) {
|
||||
c.debugMode = true;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../../components.dart';
|
||||
import '../../../extensions.dart';
|
||||
import '../../components/mixins/draggable.dart';
|
||||
import '../../components/mixins/tapable.dart';
|
||||
@ -27,7 +28,9 @@ bool hasAdvancedGesturesDetectors(Game game) =>
|
||||
game is HasDraggableComponents;
|
||||
|
||||
bool hasMouseDetectors(Game game) =>
|
||||
game is MouseMovementDetector || game is ScrollDetector;
|
||||
game is MouseMovementDetector ||
|
||||
game is ScrollDetector ||
|
||||
game is HasHoverableComponents;
|
||||
|
||||
Widget applyBasicGesturesDetectors(Game game, Widget child) {
|
||||
return GestureDetector(
|
||||
@ -267,12 +270,13 @@ Widget applyAdvancedGesturesDetectors(Game game, Widget child) {
|
||||
}
|
||||
|
||||
Widget applyMouseDetectors(Game game, Widget child) {
|
||||
final mouseMoveFn = game is MouseMovementDetector
|
||||
? game.onMouseMove
|
||||
: (game is HasHoverableComponents ? game.onMouseMove : null);
|
||||
return Listener(
|
||||
child: MouseRegion(
|
||||
child: child,
|
||||
onHover: game is MouseMovementDetector
|
||||
? (e) => game.onMouseMove(PointerHoverInfo.fromDetails(game, e))
|
||||
: null,
|
||||
onHover: (e) => mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e)),
|
||||
),
|
||||
onPointerSignal: (event) =>
|
||||
game is ScrollDetector && event is PointerScrollEvent
|
||||
|
||||
@ -24,7 +24,7 @@ void main() {
|
||||
test('fail to parse invalid anchor', () {
|
||||
expect(
|
||||
() => Anchor.valueOf('foobar'),
|
||||
throwsA(const TypeMatcher<AssertionError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -28,15 +28,11 @@ void main() {
|
||||
|
||||
final game2 = _GameWithoutDraggables();
|
||||
game2.onResize(Vector2.all(100));
|
||||
var hasError = false;
|
||||
|
||||
try {
|
||||
await game2.add(DraggableComponent());
|
||||
} catch (e) {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
expect(hasError, true);
|
||||
expect(
|
||||
() => game2.add(DraggableComponent()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('can be dragged', () async {
|
||||
|
||||
162
packages/flame/test/components/hoverable_test.dart
Normal file
162
packages/flame/test/components/hoverable_test.dart
Normal file
@ -0,0 +1,162 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/components/mixins/hoverable.dart';
|
||||
import 'package:flame/src/gestures/events.dart';
|
||||
import 'package:flutter/gestures.dart' show PointerHoverEvent;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _GameWithHoverables extends BaseGame with HasHoverableComponents {}
|
||||
|
||||
class _GameWithoutHoverables extends BaseGame {}
|
||||
|
||||
class HoverableComponent extends PositionComponent with Hoverable {
|
||||
int enterCount = 0;
|
||||
int leaveCount = 0;
|
||||
|
||||
@override
|
||||
void onHoverEnter(PointerHoverInfo event) {
|
||||
enterCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverLeave(PointerHoverInfo event) {
|
||||
leaveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('hoverable test', () {
|
||||
test('make sure they cannot be added to invalid games', () async {
|
||||
final game1 = _GameWithHoverables();
|
||||
game1.onResize(Vector2.all(100));
|
||||
// should be ok
|
||||
await game1.add(HoverableComponent());
|
||||
|
||||
final game2 = _GameWithoutHoverables();
|
||||
game2.onResize(Vector2.all(100));
|
||||
|
||||
expect(
|
||||
() => game2.add(HoverableComponent()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
test('single component', () async {
|
||||
final game = _GameWithHoverables();
|
||||
game.onResize(Vector2.all(100));
|
||||
|
||||
final c = HoverableComponent()
|
||||
..position = Vector2(10, 20)
|
||||
..size = Vector2(3, 3);
|
||||
await game.add(c);
|
||||
game.update(0);
|
||||
|
||||
expect(c.isHovered, false);
|
||||
expect(c.enterCount, 0);
|
||||
expect(c.leaveCount, 0);
|
||||
|
||||
_triggerMouseMove(game, 0, 0);
|
||||
expect(c.isHovered, false);
|
||||
expect(c.enterCount, 0);
|
||||
expect(c.leaveCount, 0);
|
||||
|
||||
_triggerMouseMove(game, 11, 0);
|
||||
expect(c.isHovered, false);
|
||||
expect(c.enterCount, 0);
|
||||
expect(c.leaveCount, 0);
|
||||
|
||||
_triggerMouseMove(game, 11, 21); // enter!
|
||||
expect(c.isHovered, true);
|
||||
expect(c.enterCount, 1);
|
||||
expect(c.leaveCount, 0);
|
||||
|
||||
_triggerMouseMove(game, 12, 22); // still inside
|
||||
expect(c.isHovered, true);
|
||||
expect(c.enterCount, 1);
|
||||
expect(c.leaveCount, 0);
|
||||
|
||||
_triggerMouseMove(game, 11, 25); // leave
|
||||
expect(c.isHovered, false);
|
||||
expect(c.enterCount, 1);
|
||||
expect(c.leaveCount, 1);
|
||||
|
||||
_triggerMouseMove(game, 11, 21); // enter again
|
||||
expect(c.isHovered, true);
|
||||
expect(c.enterCount, 2);
|
||||
expect(c.leaveCount, 1);
|
||||
});
|
||||
test('camera is respected', () async {
|
||||
final game = _GameWithHoverables();
|
||||
game.onResize(Vector2.all(100));
|
||||
|
||||
final c = HoverableComponent()
|
||||
..position = Vector2(10, 20)
|
||||
..size = Vector2(3, 3);
|
||||
await game.add(c);
|
||||
game.update(0);
|
||||
|
||||
// component is now at the corner of the screen
|
||||
game.camera.snapTo(Vector2(10, 20));
|
||||
|
||||
_triggerMouseMove(game, 11, 21);
|
||||
expect(c.isHovered, false);
|
||||
_triggerMouseMove(game, 11, 1);
|
||||
expect(c.isHovered, false);
|
||||
_triggerMouseMove(game, 1, 1);
|
||||
expect(c.isHovered, true);
|
||||
_triggerMouseMove(game, 5, 1);
|
||||
expect(c.isHovered, false);
|
||||
});
|
||||
test('multiple components', () async {
|
||||
final game = _GameWithHoverables();
|
||||
game.onResize(Vector2.all(100));
|
||||
|
||||
final a = HoverableComponent()
|
||||
..position = Vector2(10, 0)
|
||||
..size = Vector2(2, 20);
|
||||
final b = HoverableComponent()
|
||||
..position = Vector2(10, 10)
|
||||
..size = Vector2(2, 2);
|
||||
final c = HoverableComponent()
|
||||
..position = Vector2(0, 7)
|
||||
..size = Vector2(20, 2);
|
||||
await game.add(a);
|
||||
await game.add(b);
|
||||
await game.add(c);
|
||||
game.update(0);
|
||||
|
||||
_triggerMouseMove(game, 0, 0);
|
||||
expect(a.isHovered, false);
|
||||
expect(b.isHovered, false);
|
||||
expect(c.isHovered, false);
|
||||
|
||||
_triggerMouseMove(game, 10, 10);
|
||||
expect(a.isHovered, true);
|
||||
expect(b.isHovered, true);
|
||||
expect(c.isHovered, false);
|
||||
|
||||
_triggerMouseMove(game, 11, 8);
|
||||
expect(a.isHovered, true);
|
||||
expect(b.isHovered, false);
|
||||
expect(c.isHovered, true);
|
||||
|
||||
_triggerMouseMove(game, 11, 6);
|
||||
expect(a.isHovered, true);
|
||||
expect(b.isHovered, false);
|
||||
expect(c.isHovered, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(luan) we can probably provide some helpers to facilitate testing events
|
||||
void _triggerMouseMove(HasHoverableComponents game, double dx, double dy) {
|
||||
game.onMouseMove(
|
||||
PointerHoverInfo.fromDetails(
|
||||
game,
|
||||
PointerHoverEvent(
|
||||
position: Offset(dx, dy),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -19,15 +19,10 @@ void main() {
|
||||
final game2 = _GameWithoutTapables();
|
||||
game2.onResize(Vector2.all(100));
|
||||
|
||||
var hasError = false;
|
||||
|
||||
try {
|
||||
await game2.add(TapableComponent());
|
||||
} catch (e) {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
expect(hasError, true);
|
||||
expect(
|
||||
() => game2.add(TapableComponent()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user