Add hoverables (#797)

This commit is contained in:
Luan Nico
2021-05-21 09:04:12 -04:00
committed by GitHub
parent 8f7c773f82
commit 9c1808ca92
12 changed files with 302 additions and 34 deletions

View File

@ -138,10 +138,10 @@ class MyGame extends Game with TapDetector {
You can also check more complete examples You can also check more complete examples
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/controls/). [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 Any component derived from `BaseComponent` (most components) can add the `Tapable`, the
`Draggable` mixins to handle taps and drags on the component. `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 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 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: Minimal component example:
```dart ```dart
import 'package:flame/components/component.dart'; import 'package:flame/components.dart';
import 'package:flame/components/mixins/tapable.dart';
class TapableComponent extends PositionComponent with Tapable { 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): multi-drag):
```dart ```dart
import 'package:flame/components/component.dart'; import 'package:flame/components.dart';
import 'package:flame/components/mixins/draggable.dart';
class DraggableComponent extends PositionComponent with Draggable { 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 **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. 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 ## Hitbox
The `Hitbox` mixin is used to make detection of gestures on top of your `PositionComponent`s more 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 accurate. Say that you have a fairly round rock as a `SpriteComponent` for example, then you don't

View File

@ -4,6 +4,7 @@ import 'package:flame/game.dart';
import '../../commons/commons.dart'; import '../../commons/commons.dart';
import 'advanced_joystick.dart'; import 'advanced_joystick.dart';
import 'draggables.dart'; import 'draggables.dart';
import 'hoverables.dart';
import 'joystick.dart'; import 'joystick.dart';
import 'keyboard.dart'; import 'keyboard.dart';
import 'mouse_movement.dart'; import 'mouse_movement.dart';
@ -55,16 +56,18 @@ void addControlsStories(Dashbook dashbook) {
(context) { (context) {
return GameWidget( return GameWidget(
game: DraggablesGame( game: DraggablesGame(
zoom: context.listProperty( zoom: context.listProperty('zoom', 1, [0.5, 1, 1.5]),
'zoom',
1,
[0.5, 1, 1.5],
),
), ),
); );
}, },
codeLink: baseLink('controls/draggables.dart'), 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( ..add(
'Joystick', 'Joystick',
(_) => GameWidget(game: JoystickGame()), (_) => GameWidget(game: JoystickGame()),

View 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));
}
}

View File

@ -11,6 +11,7 @@
- No need to send size in ParallaxComponent.fromParallax since Parallax already contains it - No need to send size in ParallaxComponent.fromParallax since Parallax already contains it
- Fix Text Rendering not working properly - Fix Text Rendering not working properly
- Add more useful methods to the IsometricTileMap component - Add more useful methods to the IsometricTileMap component
- Add Hoverables
## [1.0.0-rc10] ## [1.0.0-rc10]
- Updated tutorial documentation to indicate use of new version - Updated tutorial documentation to indicate use of new version

View File

@ -8,6 +8,7 @@ export 'src/components/mixins/draggable.dart';
export 'src/components/mixins/has_collidables.dart'; export 'src/components/mixins/has_collidables.dart';
export 'src/components/mixins/has_game_ref.dart'; export 'src/components/mixins/has_game_ref.dart';
export 'src/components/mixins/hitbox.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/single_child_particle.dart';
export 'src/components/mixins/tapable.dart'; export 'src/components/mixins/tapable.dart';
export 'src/components/nine_tile_box_component.dart'; export 'src/components/nine_tile_box_component.dart';

View 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);
}
}
}
}

View File

@ -11,6 +11,7 @@ import '../components/mixins/collidable.dart';
import '../components/mixins/draggable.dart'; import '../components/mixins/draggable.dart';
import '../components/mixins/has_collidables.dart'; import '../components/mixins/has_collidables.dart';
import '../components/mixins/has_game_ref.dart'; import '../components/mixins/has_game_ref.dart';
import '../components/mixins/hoverable.dart';
import '../components/mixins/tapable.dart'; import '../components/mixins/tapable.dart';
import '../components/position_component.dart'; import '../components/position_component.dart';
import '../fps_counter.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', '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) { if (debugMode && c is PositionComponent) {
c.debugMode = true; c.debugMode = true;

View File

@ -1,6 +1,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../../../components.dart';
import '../../../extensions.dart'; import '../../../extensions.dart';
import '../../components/mixins/draggable.dart'; import '../../components/mixins/draggable.dart';
import '../../components/mixins/tapable.dart'; import '../../components/mixins/tapable.dart';
@ -27,7 +28,9 @@ bool hasAdvancedGesturesDetectors(Game game) =>
game is HasDraggableComponents; game is HasDraggableComponents;
bool hasMouseDetectors(Game game) => 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) { Widget applyBasicGesturesDetectors(Game game, Widget child) {
return GestureDetector( return GestureDetector(
@ -267,12 +270,13 @@ Widget applyAdvancedGesturesDetectors(Game game, Widget child) {
} }
Widget applyMouseDetectors(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( return Listener(
child: MouseRegion( child: MouseRegion(
child: child, child: child,
onHover: game is MouseMovementDetector onHover: (e) => mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e)),
? (e) => game.onMouseMove(PointerHoverInfo.fromDetails(game, e))
: null,
), ),
onPointerSignal: (event) => onPointerSignal: (event) =>
game is ScrollDetector && event is PointerScrollEvent game is ScrollDetector && event is PointerScrollEvent

View File

@ -24,7 +24,7 @@ void main() {
test('fail to parse invalid anchor', () { test('fail to parse invalid anchor', () {
expect( expect(
() => Anchor.valueOf('foobar'), () => Anchor.valueOf('foobar'),
throwsA(const TypeMatcher<AssertionError>()), throwsA(isA<AssertionError>()),
); );
}); });

View File

@ -28,15 +28,11 @@ void main() {
final game2 = _GameWithoutDraggables(); final game2 = _GameWithoutDraggables();
game2.onResize(Vector2.all(100)); game2.onResize(Vector2.all(100));
var hasError = false;
try { expect(
await game2.add(DraggableComponent()); () => game2.add(DraggableComponent()),
} catch (e) { throwsA(isA<AssertionError>()),
hasError = true; );
}
expect(hasError, true);
}); });
test('can be dragged', () async { test('can be dragged', () async {

View 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),
),
),
);
}

View File

@ -19,15 +19,10 @@ void main() {
final game2 = _GameWithoutTapables(); final game2 = _GameWithoutTapables();
game2.onResize(Vector2.all(100)); game2.onResize(Vector2.all(100));
var hasError = false; expect(
() => game2.add(TapableComponent()),
try { throwsA(isA<AssertionError>()),
await game2.add(TapableComponent()); );
} catch (e) {
hasError = true;
}
expect(hasError, true);
}); });
}); });
} }