mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 04:18:25 +08:00
feat: Add DoubleTapCallbacks that receives double-tap events. (#2327)
As-is As mentioned in #2321, the user needs to propagate double-tap events to the component tree using DoubleTapDetector & propagateToChildren until now. To-be Any components that are mixed into the DoubleTapCallbacks receive double-tap-related events. Same as DragCallbacks, there is no need to add mixin to the game like HasDoubleTapCallbaks as before.
This commit is contained in:
committed by
GitHub
parent
0c14d4cb87
commit
b5f79d1ce4
@ -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
|
||||
|
||||
65
examples/lib/stories/input/double_tap_callbacks_example.dart
Normal file
65
examples/lib/stories/input/double_tap_callbacks_example.dart
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
@ -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()),
|
||||
|
||||
@ -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'
|
||||
@ -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;
|
||||
|
||||
@ -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 {}
|
||||
@ -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(),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import 'package:flame/src/events/messages/event.dart';
|
||||
|
||||
class DoubleTapEvent extends Event {}
|
||||
@ -0,0 +1,190 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/experimental.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/double_tap_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('DoubleTapCallbacks', () {
|
||||
testWidgets(
|
||||
'receives double-tap event',
|
||||
(tester) async {
|
||||
final component = _DoubleTapCallbacksComponent()
|
||||
..position = Vector2.all(10);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: FlameGame(children: [component]),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final gesture = await tester.createGesture();
|
||||
await gesture.down(const Offset(10, 10));
|
||||
await tester.pump();
|
||||
|
||||
await gesture.up();
|
||||
expect(component.doubleTapDown, 0);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await gesture.down(const Offset(10, 10));
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await gesture.up();
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 1);
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'''does not receive an event when double-tapping a position far from the component''',
|
||||
(tester) async {
|
||||
final component = _DoubleTapCallbacksComponent()
|
||||
..position = Vector2.all(10);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: FlameGame(children: [component]),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final gesture = await tester.createGesture();
|
||||
await gesture.down(const Offset(100, 100));
|
||||
await gesture.up();
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await gesture.down(const Offset(100, 100));
|
||||
|
||||
expect(component.doubleTapDown, 0);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await gesture.up();
|
||||
|
||||
expect(component.doubleTapDown, 0);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'receives a cancel event when gesture is canceled by drag',
|
||||
(tester) async {
|
||||
final component = _DoubleTapCallbacksComponent()
|
||||
..position = Vector2.all(10);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: FlameGame(children: [component]),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final gesture = await tester.createGesture();
|
||||
await gesture.down(const Offset(10, 10));
|
||||
await gesture.up();
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await gesture.down(const Offset(10, 10));
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await gesture.moveBy(const Offset(100, 100));
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 1);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'receives a cancel event when gesture is canceled by cancel',
|
||||
(tester) async {
|
||||
final component = _DoubleTapCallbacksComponent()
|
||||
..position = Vector2.all(10);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: FlameGame(children: [component]),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final gesture = await tester.createGesture();
|
||||
await gesture.down(const Offset(10, 10));
|
||||
await gesture.up();
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await gesture.down(const Offset(10, 10));
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 0);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await gesture.cancel();
|
||||
|
||||
expect(component.doubleTapDown, 1);
|
||||
expect(component.doubleTapCancel, 1);
|
||||
expect(component.doubleTap, 0);
|
||||
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
},
|
||||
);
|
||||
|
||||
testWithFlameGame(
|
||||
'DoubleTapDispatcher is added to game when the callback is mounted',
|
||||
(game) async {
|
||||
final component = _DoubleTapCallbacksComponent();
|
||||
await game.add(component);
|
||||
await game.ready();
|
||||
|
||||
expect(game.firstChild<DoubleTapDispatcher>(), isNotNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _DoubleTapCallbacksComponent extends PositionComponent
|
||||
with DoubleTapCallbacks {
|
||||
_DoubleTapCallbacksComponent() {
|
||||
anchor = Anchor.center;
|
||||
size = Vector2.all(10);
|
||||
}
|
||||
|
||||
int doubleTapDown = 0;
|
||||
int doubleTapCancel = 0;
|
||||
int doubleTap = 0;
|
||||
|
||||
@override
|
||||
void onDoubleTapUp(DoubleTapEvent event) {
|
||||
doubleTap++;
|
||||
}
|
||||
|
||||
@override
|
||||
void onDoubleTapCancel(DoubleTapCancelEvent event) {
|
||||
doubleTapCancel++;
|
||||
}
|
||||
|
||||
@override
|
||||
void onDoubleTapDown(DoubleTapDownEvent event) {
|
||||
doubleTapDown++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user