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
|
### GestureHitboxes
|
||||||
|
|
||||||
The `GestureHitboxes` mixin is used to more accurately recognize gestures on top of your
|
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:dashbook/dashbook.dart';
|
||||||
import 'package:examples/commons/commons.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/draggables_example.dart';
|
||||||
import 'package:examples/stories/input/gesture_hitboxes_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/hardware_keyboard_example.dart';
|
||||||
@ -38,6 +39,16 @@ void addInputStories(Dashbook dashbook) {
|
|||||||
codeLink: baseLink('input/draggables_example.dart'),
|
codeLink: baseLink('input/draggables_example.dart'),
|
||||||
info: DraggablesExample.description,
|
info: DraggablesExample.description,
|
||||||
)
|
)
|
||||||
|
..add(
|
||||||
|
'Double Tap (Component)',
|
||||||
|
(context) {
|
||||||
|
return GameWidget(
|
||||||
|
game: DoubleTapCallbacksExample(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codeLink: baseLink('input/draggables_example.dart'),
|
||||||
|
info: DoubleTapCallbacksExample.description,
|
||||||
|
)
|
||||||
..add(
|
..add(
|
||||||
'Hoverables',
|
'Hoverables',
|
||||||
(_) => GameWidget(game: HoverablesExample()),
|
(_) => 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/fixed_size_viewport.dart' show FixedSizeViewport;
|
||||||
export 'src/camera/viewports/max_viewport.dart' show MaxViewport;
|
export 'src/camera/viewports/max_viewport.dart' show MaxViewport;
|
||||||
export 'src/camera/world.dart' show World;
|
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/drag_callbacks.dart' show DragCallbacks;
|
||||||
export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks;
|
export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks;
|
||||||
export 'src/events/flame_game_mixins/has_draggable_components.dart'
|
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;
|
show HasTappableComponents;
|
||||||
export 'src/events/flame_game_mixins/has_tappables_bridge.dart'
|
export 'src/events/flame_game_mixins/has_tappables_bridge.dart'
|
||||||
show HasTappablesBridge;
|
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_cancel_event.dart' show DragCancelEvent;
|
||||||
export 'src/events/messages/drag_end_event.dart' show DragEndEvent;
|
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_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