Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add DoubleTapCallbacks that receives double-tap events. #2327

Merged
merged 21 commits into from Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/flame/inputs/gesture_input.md
Expand Up @@ -473,6 +473,31 @@ class MyGame extends FlameGame with HasHoverables {
```


### DoubleTapCallbacks

Flame also offers a mixin named `DoubleTapCallbacks` to receive a double-tap event from the
component. To start receiving double tap events in a component, add the
`DoubleTapCallbacks` mixin to your `PositionComponent`.

```dart
class MyComponent extends PositionComponent with DoubleTapCallbacks {
@override
void onDoubleTapUp(DoubleTapEvent event) {
/// Do something
}

@override
void onDoubleTapCancel(DoubleTapCancelEvent event) {
/// Do something
}

@override
void onDoubleTapDown(DoubleTapDownEvent event) {
/// Do something
}
```


### GestureHitboxes

The `GestureHitboxes` mixin is used to more accurately recognize gestures on top of your
Expand Down
65 changes: 65 additions & 0 deletions examples/lib/stories/input/double_tap_callbacks_example.dart
@@ -0,0 +1,65 @@
import 'package:examples/commons/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

class DoubleTapCallbacksExample extends FlameGame with DoubleTapCallbacks {
static const String description = '''
In this example, we show how you can use the `DoubleTapCallbacks` mixin on
a `Component`. Double tap Ember and see her color changing.
The example also adds white circles when double-tapping on the game area.
''';

@override
Future<void> onLoad() async {
children.register<DoubleTappableEmber>();
}

@override
void onGameResize(Vector2 canvasSize) {
children
.query<DoubleTappableEmber>()
.forEach((element) => element.removeFromParent());
add(DoubleTappableEmber(position: canvasSize / 2));

super.onGameResize(canvasSize);
}

@override
void onDoubleTapDown(DoubleTapDownEvent event) {
add(
CircleComponent(
radius: 30,
position: event.localPosition,
anchor: Anchor.center,
),
);
}
}

class DoubleTappableEmber extends Ember with DoubleTapCallbacks {
@override
bool debugMode = true;

DoubleTappableEmber({Vector2? position})
: super(
position: position ?? Vector2.all(100),
size: Vector2.all(100),
);

@override
void onDoubleTapUp(DoubleTapEvent event) {
debugColor = Colors.greenAccent;
}

@override
void onDoubleTapCancel(DoubleTapCancelEvent event) {
debugColor = Colors.red;
}

@override
void onDoubleTapDown(DoubleTapDownEvent event) {
debugColor = Colors.blue;
}
}
11 changes: 11 additions & 0 deletions examples/lib/stories/input/input.dart
@@ -1,5 +1,6 @@
import 'package:dashbook/dashbook.dart';
import 'package:examples/commons/commons.dart';
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';
Expand Down Expand Up @@ -38,6 +39,16 @@ void addInputStories(Dashbook dashbook) {
codeLink: baseLink('input/draggables_example.dart'),
info: DraggablesExample.description,
)
..add(
'Double Tap (Component)',
(context) {
return GameWidget(
game: DoubleTapCallbacksExample(),
);
},
codeLink: baseLink('input/draggables_example.dart'),
info: DoubleTapCallbacksExample.description,
)
..add(
'Hoverables',
(_) => GameWidget(game: HoverablesExample()),
Expand Down
6 changes: 6 additions & 0 deletions packages/flame/lib/experimental.dart
Expand Up @@ -21,6 +21,8 @@ export 'src/camera/viewports/fixed_aspect_ratio_viewport.dart'
export 'src/camera/viewports/fixed_size_viewport.dart' show FixedSizeViewport;
export 'src/camera/viewports/max_viewport.dart' show MaxViewport;
export 'src/camera/world.dart' show World;
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/tap_callbacks.dart' show TapCallbacks;
export 'src/events/flame_game_mixins/has_draggable_components.dart'
Expand All @@ -32,6 +34,10 @@ export 'src/events/flame_game_mixins/has_tappable_components.dart'
show HasTappableComponents;
export 'src/events/flame_game_mixins/has_tappables_bridge.dart'
show HasTappablesBridge;
export 'src/events/messages/double_tap_cancel_event.dart'
show DoubleTapCancelEvent;
export 'src/events/messages/double_tap_down_event.dart' show DoubleTapDownEvent;
export 'src/events/messages/double_tap_event.dart' show DoubleTapEvent;
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;
Expand Down
@@ -0,0 +1,37 @@
import 'package:flame/game.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/events/flame_game_mixins/double_tap_dispatcher.dart';
import 'package:flame/src/events/messages/double_tap_cancel_event.dart';
import 'package:flame/src/events/messages/double_tap_down_event.dart';
import 'package:flame/src/events/messages/double_tap_event.dart';

/// [DoubleTapCallbacks] adds the ability to receive double-tap events in a
/// component.
///
/// In addition to adding this mixin, the component must also implement the
/// [containsLocalPoint] method.
///
/// At present, flutter detects only one double-tap events simultaneously.
/// This means that if you're double-tapping two [DoubleTapCallbacks] located
/// far away from each other, only one callback will be fired (or none).
mixin DoubleTapCallbacks on Component {
/// This triggers when the pointer stops contacting the device after the
/// second tap.
void onDoubleTapUp(DoubleTapEvent event) {}

/// This triggers immediately after the down event of the second tap.
void onDoubleTapDown(DoubleTapDownEvent event) {}

/// This triggers once the gesture loses the arena if [onDoubleTapDown] has
/// previously been triggered.
void onDoubleTapCancel(DoubleTapCancelEvent event) {}

@override
void onMount() {
super.onMount();
final game = findGame()! as FlameGame;
if (game.firstChild<DoubleTapDispatcher>() == null) {
game.add(DoubleTapDispatcher());
}
}
}
@@ -0,0 +1,66 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/src/events/component_mixins/double_tap_callbacks.dart';
import 'package:flame/src/events/messages/double_tap_cancel_event.dart';
import 'package:flame/src/events/messages/double_tap_down_event.dart';
import 'package:flame/src/events/messages/double_tap_event.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';

/// [DoubleTapDispatcher] propagates double-tap events to every components in
/// the component tree that is mixed with [DoubleTapCallbacks]. This will be
/// attached to the [FlameGame] instance automatically whenever any
/// [DoubleTapCallbacks] are mounted into the component tree.
@internal
class DoubleTapDispatcher extends Component with HasGameRef<FlameGame> {
final _components = <DoubleTapCallbacks>{};
bool _eventHandlerRegistered = false;

void _onDoubleTapDown(DoubleTapDownEvent event) {
event.deliverAtPoint(
rootComponent: game,
eventHandler: (DoubleTapCallbacks component) {
_components.add(component..onDoubleTapDown(event));
},
);
}

void _onDoubleTapUp(DoubleTapEvent event) {
_components.forEach((component) => component.onDoubleTapUp(event));
_components.clear();
}

void _onDoubleTapCancel(DoubleTapCancelEvent event) {
_components.forEach((component) => component.onDoubleTapCancel(event));
_components.clear();
}

@override
void onMount() {
if (game.firstChild<DoubleTapDispatcher>() == null) {
game.gestureDetectors.add(
DoubleTapGestureRecognizer.new,
(DoubleTapGestureRecognizer instance) {
instance.onDoubleTapDown =
(details) => _onDoubleTapDown(DoubleTapDownEvent(details));
instance.onDoubleTapCancel =
() => _onDoubleTapCancel(DoubleTapCancelEvent());
instance.onDoubleTap = () => _onDoubleTapUp(DoubleTapEvent());
},
);
_eventHandlerRegistered = true;
} else {
removeFromParent();
}
}

@override
void onRemove() {
if (!_eventHandlerRegistered) {
return;
}

game.gestureDetectors.remove<DoubleTapGestureRecognizer>();
_eventHandlerRegistered = false;
}
}
@@ -0,0 +1,3 @@
import 'package:flame/src/events/messages/event.dart';

class DoubleTapCancelEvent extends Event {}
14 changes: 14 additions & 0 deletions packages/flame/lib/src/events/messages/double_tap_down_event.dart
@@ -0,0 +1,14 @@
import 'package:flame/extensions.dart';
import 'package:flame/src/events/messages/position_event.dart';
import 'package:flutter/gestures.dart';

class DoubleTapDownEvent extends PositionEvent {
final PointerDeviceKind deviceKind;

DoubleTapDownEvent(TapDownDetails details)
: deviceKind = details.kind ?? PointerDeviceKind.unknown,
super(
canvasPosition: details.localPosition.toVector2(),
devicePosition: details.globalPosition.toVector2(),
);
}
3 changes: 3 additions & 0 deletions packages/flame/lib/src/events/messages/double_tap_event.dart
@@ -0,0 +1,3 @@
import 'package:flame/src/events/messages/event.dart';

class DoubleTapEvent extends Event {}