Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions doc/flame/inputs/gesture_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@ Detectors will be deprecated in the future. Prefer `Callbacks` instead.
- onHorizontalDragEnd
- onHorizontalDragCancel

- ForcePressDetector
- onForcePressStart
- onForcePressPeak
- onForcePressUpdate
- onForcePressEnd

- PanDetector
- onPanDown
- onPanStart
Expand Down
49 changes: 49 additions & 0 deletions doc/flame/inputs/tap_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,55 @@ class MyComponent extends PositionComponent with DoubleTapCallbacks {
```


### ForcePressCallbacks

The `ForcePressCallbacks` mixin gives a component access to force press gestures, i.e. touches that
report how hard the user is pressing.

```{warning}
Force press requires a pressure-sensitive screen: Apple's 3D Touch, which
shipped on the iPhone 6s through the iPhone XS, or a small number of Android
devices. On every other device the gesture is never recognized and these
callbacks never fire.
```

All four callbacks receive the same `ForcePressEvent`, whose `pressure` is normalized to the
`[0, 1]` range across the pressure range the device reports:

```dart
class MyComponent extends PositionComponent with ForcePressCallbacks {
MyComponent() : super(size: Vector2.all(100));

@override
void onForcePressStart(ForcePressEvent event) {
super.onForcePressStart(event);
// The press crossed the threshold at which the gesture is recognized.
}

@override
void onForcePressPeak(ForcePressEvent event) {
// The press crossed the "peak" pressure threshold.
}

@override
void onForcePressUpdate(ForcePressEvent event) {
scale = Vector2.all(1 + event.pressure);
}

@override
void onForcePressEnd(ForcePressEvent event) {
super.onForcePressEnd(event);
scale = Vector2.all(1);
}
}
```

Only the topmost component under the point of contact receives `onForcePressStart`; set
`event.continuePropagation` to true to let it through to the components below. Once a component has
accepted the gesture it keeps receiving the peak, update and end events even if the pointer moves
outside its bounds. The `isForcePressed` getter is true while a gesture is active.


## Migration

If you have an existing game that uses `Tappable`/`Draggable` mixins, then this section will
Expand Down
41 changes: 41 additions & 0 deletions doc/flame/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,47 @@ major versions of Flame, together with the steps required to migrate your code.
## Migrating from v1.38.0 to v2.0.0


### `ForcePressDetector` replaced by `ForcePressCallbacks`

Force press was the last gesture that only existed on the old game-level detector API. The
`ForcePressDetector` mixin and its `ForcePressInfo` event class have been removed, and replaced by
the `ForcePressCallbacks` component mixin and the `ForcePressEvent` class, in line with every other
gesture.

The four callbacks keep their names, but they now receive a single `ForcePressEvent` instead of a
`ForcePressInfo`, and they are declared on a component rather than on the game:

```dart
// Before
class MyGame extends FlameGame with ForcePressDetector {
@override
void onForcePressUpdate(ForcePressInfo info) {
final position = info.eventPosition.widget;
final pressure = info.pressure;
}
}

// After
class MyComponent extends PositionComponent with ForcePressCallbacks {
@override
void onForcePressUpdate(ForcePressEvent event) {
final position = event.localPosition;
final pressure = event.pressure;
}
}
```

As with the other callback mixins, the event is delivered only to components under the point of
contact, `event.localPosition` is available in addition to `canvasPosition` and `devicePosition`,
and propagation past the topmost component is opt-in via `event.continuePropagation`.

Note that force press requires a pressure-sensitive screen — Apple's 3D Touch, which shipped on the
iPhone 6s through the
[iPhone XS](https://support.apple.com/guide/iphone/aside/iph945ccc462/14.0/ios/14.0), or a small
number of Android devices. On any other device the gesture is never recognized, so these callbacks
never fire. This is unchanged from the old API.


### `onDragCancel` no longer delegates to `onDragEnd`

`DragCallbacks.onDragCancel` used to convert the cancellation into an `onDragEnd` event by default,
Expand Down
7 changes: 5 additions & 2 deletions packages/flame/lib/events.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export 'src/events/callbacks/double_tap_callbacks.dart' show DoubleTapCallbacks;
export 'src/events/callbacks/drag_callbacks.dart' show DragCallbacks;
export 'src/events/callbacks/force_press_callbacks.dart'
show ForcePressCallbacks;
export 'src/events/callbacks/hover_callbacks.dart' show HoverCallbacks;
export 'src/events/callbacks/long_press_callbacks.dart' show LongPressCallbacks;
export 'src/events/callbacks/pointer_move_callbacks.dart'
Expand All @@ -14,6 +16,8 @@ export 'src/events/callbacks/tertiary_tap_callbacks.dart'
export 'src/events/dispatchers/dispatcher.dart' show Dispatcher;
export 'src/events/dispatchers/double_tap_dispatcher.dart'
show DoubleTapDispatcher, DoubleTapDispatcherKey;
export 'src/events/dispatchers/force_press_dispatcher.dart'
show ForcePressDispatcher, ForcePressDispatcherKey;
export 'src/events/dispatchers/long_press_dispatcher.dart'
show LongPressDispatcher, LongPressDispatcherKey;
export 'src/events/dispatchers/multi_drag_scale_dispatcher.dart'
Expand Down Expand Up @@ -45,6 +49,7 @@ 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_update_event.dart' show DragUpdateEvent;
export 'src/events/messages/event.dart' show Event;
export 'src/events/messages/force_press_event.dart' show ForcePressEvent;
export 'src/events/messages/location_context_event.dart'
show LocationContextEvent;
export 'src/events/messages/long_press_cancel_event.dart'
Expand Down Expand Up @@ -81,7 +86,6 @@ export 'src/game/mixins/keyboard.dart'
export 'src/gestures/detectors.dart'
show
DoubleTapDetector,
ForcePressDetector,
HorizontalDragDetector,
LongPressDetector,
MouseMovementDetector,
Expand All @@ -97,7 +101,6 @@ export 'src/gestures/events.dart'
DragEndInfo,
DragStartInfo,
DragUpdateInfo,
ForcePressInfo,
LongPressEndInfo,
LongPressMoveUpdateInfo,
LongPressStartInfo,
Expand Down
49 changes: 49 additions & 0 deletions packages/flame/lib/src/events/callbacks/force_press_callbacks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flutter/foundation.dart';

/// This mixin can be added to a [Component] allowing it to receive force press
/// events, i.e. touches that report how hard the user is pressing.
///
/// In addition to adding this mixin, the component must also implement the
/// [containsLocalPoint] method -- only a gesture that starts on top of the
/// component will be delivered to it.
///
/// The following callbacks are available:
/// - [onForcePressStart]: the press crossed the pressure threshold at which
/// the gesture is recognized.
/// - [onForcePressPeak]: the press crossed the "peak" pressure threshold.
/// - [onForcePressUpdate]: the pressure changed during an active force press.
/// - [onForcePressEnd]: the pointer was lifted.
///
/// Note that force press requires a pressure-sensitive screen; see
/// [ForcePressEvent] for the details of which devices support it.
///
/// This callback uses [ForcePressDispatcher] to route events.
mixin ForcePressCallbacks on Component {
bool _isForcePressed = false;

/// Returns true while a force press gesture is active on this component.
bool get isForcePressed => _isForcePressed;

@mustCallSuper
void onForcePressStart(ForcePressEvent event) {
_isForcePressed = true;
}

void onForcePressPeak(ForcePressEvent event) {}

void onForcePressUpdate(ForcePressEvent event) {}

@mustCallSuper
void onForcePressEnd(ForcePressEvent event) {
_isForcePressed = false;
}

@override
@mustCallSuper
void onMount() {
super.onMount();
ForcePressDispatcher.addDispatcher(this);
}
}
119 changes: 119 additions & 0 deletions packages/flame/lib/src/events/dispatchers/force_press_dispatcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';

/// **ForcePressDispatcher** facilitates dispatching of force press events to
/// the [ForcePressCallbacks] components in the component tree. It will be
/// attached to the [FlameGame] instance automatically whenever any
/// [ForcePressCallbacks] components are mounted into the component tree.
///
/// Flutter's [ForcePressGestureRecognizer] tracks a single pointer at a time,
/// so this dispatcher only needs to remember the set of components that
/// accepted the current gesture, rather than keying them by pointer id.
class ForcePressDispatcher extends Dispatcher<FlameGame> {
/// The components that received the current gesture's start event, and which
/// will therefore receive its peak, update and end events.
final Set<ForcePressCallbacks> _components = {};

@mustCallSuper
void onForcePressStart(ForcePressEvent event) {
event.deliverAtPoint(
rootComponent: game,
eventHandler: (ForcePressCallbacks component) {
_components.add(component..onForcePressStart(event));
},
);
}

@mustCallSuper
void onForcePressPeak(ForcePressEvent event) {
_forEachActiveComponent((component) => component.onForcePressPeak(event));
}

@mustCallSuper
void onForcePressUpdate(ForcePressEvent event) {
_forEachActiveComponent((component) => component.onForcePressUpdate(event));
}

@mustCallSuper
void onForcePressEnd(ForcePressEvent event) {
_forEachActiveComponent((component) => component.onForcePressEnd(event));
_components.clear();
}

/// Delivers to every component that accepted the gesture and is still
/// mounted, dropping the ones that were removed mid-gesture.
void _forEachActiveComponent(void Function(ForcePressCallbacks) handler) {
_components.removeWhere((component) => !component.isMounted);
for (final component in _components) {
handler(component);
}
}

//#region Gesture recognizer handlers

@internal
void handleForcePressStart(ForcePressDetails details) {
onForcePressStart(ForcePressEvent(game, details));
}

@internal
void handleForcePressPeak(ForcePressDetails details) {
onForcePressPeak(ForcePressEvent(game, details));
}

@internal
void handleForcePressUpdate(ForcePressDetails details) {
onForcePressUpdate(ForcePressEvent(game, details));
}

@internal
void handleForcePressEnd(ForcePressDetails details) {
onForcePressEnd(ForcePressEvent(game, details));
}

//#endregion

static void addDispatcher(Component component) {
Dispatcher.addDispatcher(
component,
const ForcePressDispatcherKey(),
ForcePressDispatcher.new,
);
}

@override
void onMount() {
game.gestureDetectors.register<ForcePressGestureRecognizer>(
ForcePressGestureRecognizer.new,
(ForcePressGestureRecognizer instance) {
instance
..onStart = handleForcePressStart
..onPeak = handleForcePressPeak
..onUpdate = handleForcePressUpdate
..onEnd = handleForcePressEnd;
},
);
super.onMount();
}

@override
void onRemove() {
game.gestureDetectors.unregister<ForcePressGestureRecognizer>();
Dispatcher.removeDispatcher(game, const ForcePressDispatcherKey());
}
}

/// Unique key for the [ForcePressDispatcher] so the game can identify it.
class ForcePressDispatcherKey implements ComponentKey {
const ForcePressDispatcherKey();

@override
int get hashCode => 'ForcePressDispatcherKey'.hashCode;

@override
bool operator ==(Object other) =>
other is ForcePressDispatcherKey && other.hashCode == hashCode;
}
39 changes: 39 additions & 0 deletions packages/flame/lib/src/events/messages/force_press_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flutter/gestures.dart';

/// The event propagated through the Flame engine during a force press gesture,
/// i.e. a touch that also reports how hard the user is pressing.
///
/// The same event class is used for all four phases of the gesture (start,
/// peak, update and end) because Flutter describes every one of them with a
/// single [ForcePressDetails] object.
///
/// This is a [PositionEvent], where the position is the point of contact.
///
/// Note that force press requires a pressure-sensitive screen: Apple's 3D
/// Touch, which shipped on the iPhone 6s through the iPhone XS, or a small
/// number of Android devices. On any other device the gesture is never
/// recognized and these events are never delivered.
class ForcePressEvent extends PositionEvent<ForcePressDetails> {
ForcePressEvent(super.game, ForcePressDetails details)
: pressure = details.pressure,
super(
raw: details,
devicePosition: details.globalPosition.toVector2(),
);

/// How hard the user is pressing, normalized to the `[0, 1]` range across
/// the pressure range that the device reports.
///
/// The gesture is only recognized once this value crosses the recognizer's
/// `startPressure` (`0.4` by default), and `onForcePressPeak` fires when it
/// crosses `peakPressure` (`0.85` by default).
final double pressure;

@override
String toString() =>
'ForcePressEvent(canvasPosition: $canvasPosition, '
'devicePosition: $devicePosition, '
'pressure: $pressure)';
}
2 changes: 1 addition & 1 deletion packages/flame/lib/src/game/flame_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ class FlameGame<W extends World> extends ComponentTreeRoot
this is LongPressDetector ||
this is VerticalDragDetector ||
this is HorizontalDragDetector ||
this is ForcePressDetector ||
this is PanDetector ||
this is ScaleDetector ||
this is MultiTapListener ||
Expand All @@ -266,6 +265,7 @@ class FlameGame<W extends World> extends ComponentTreeRoot
if (component is TapCallbacks ||
component is DragCallbacks ||
component is DoubleTapCallbacks ||
component is ForcePressCallbacks ||
component is ScaleCallbacks ||
Comment on lines 265 to 269

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrong, it does work at game level, because componentsAtPoint includes the game itself
that being said there are other new detectors missing here, which is indeed a bug, and this is not tested, but it is not tested for other detectors either
so I will followup with a broad approach here, but not part of this PR

component is SecondaryTapCallbacks) {
return true;
Expand Down
Loading
Loading