I am trying to read the stylus with a "global" listener, while still being able to interact with the rest of the UI with the finger. The event object passed to the listeners of the Listener
widget actually has a property for the device kind, but I can't tell it which events to absorb and which not to. You can only specify it for every event with HitTestBehavior
, but this is not what I want.
I tried a bit to reverse engineer the Listener
widget, but it doesn't seem to be possible to know the pointer device kind at the point where you have to decide whether to fire a hit. And I also could not find out how to cancel an event in the handleEvent
callback provided by RenderObject
or something like that.
Listener(
onPointerDown: (event) {
if (!pointerKinds.contains(event.kind)) return;
// Absorb now
...
},
);
class _SomeRenderObject extends RenderProxyBox {
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
if(event.kind != PointerDeviceKind.stylus) {
// Cancel event
}
}
}
Turns out, the mechanism I was searching for is built on Listener
and is called gesture-disambiguation.
Its API is exposed through RawGestureDetector
and GestureRecognizer
s. These are used under the hood of GestureDetector
for example. Listener
itself is actually rarely used to listen to events.
A GestureRecognizer
needs to decide whether or not some user interaction fits a specific gesture and when it does fit, it can claim any pointers that it needs, so no other GestureRecognizer
can claim these pointers.
There are already many implementations available in flutter, like DragGestureRecognizer
and it turns out, they already can filter for specific PointerDeviceKind
s. The Constructor has a supportedDevices
property you can use. But for some reason, you can't use it in GestureDetector
directly, but you have to use RawGestureDetector
, where you have to construct the GestureRecognizer
s yourself. Here is an example:
Widget build(BuildContext context) {
Map<Type, GestureRecognizerFactory> gestures = {
DragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DragGestureRecognizer>(
() => DragGestureRecognizer(supportedDevices: {PointerDeviceKind.stylus})
..onStart = _onStart
..onUpdate = _onUpdate
..onEnd = _onEnd,
(instance) => instance
..onStart = _onStart
..onUpdate = _onUpdate
..onEnd = _onEnd,
),
};
return RawGestureDetector(
child: child,
gestures: gestures,
);
}
It is a bit more Boilerplate involved thou!
But I went a bit further because I didn't want the drag gesture to start, after the pointer has moved, but rather to start in the moment, the pointer touches the screen, and implemented my own GestureRecognizer
(based on what I found in DragGestureRecognizer
):
class InstantDragGestureRecognizer extends OneSequenceGestureRecognizer {
GestureDragStartCallback? onStart;
GestureDragUpdateCallback? onUpdate;
GestureDragEndCallback? onEnd;
Duration? _startTimestamp;
late OffsetPair _initialPosition;
int? _pointer;
InstantDragGestureRecognizer({
Object? debugOwner,
Set<PointerDeviceKind>? supportedDevices,
}) : super(supportedDevices: supportedDevices);
@override
String get debugDescription => "instant-drag";
@override
void didStopTrackingLastPointer(int pointer) {
_pointer = null;
_checkEnd(pointer);
}
// called for every event that involves a pointer, tracked by this recognizer
@override
void handleEvent(PointerEvent event) {
_startTimestamp = event.timeStamp;
if (event is PointerMoveEvent) {
_checkUpdate(
sourceTimeStamp: event.timeStamp,
delta: event.localDelta,
primaryDelta: null,
globalPosition: event.position,
localPosition: event.localPosition,
);
}
if (event is PointerUpEvent || event is PointerCancelEvent)
stopTrackingPointer(event.pointer);
}
// new pointer touches the screen and needs to be registered for gesture
// tracking, override [isPointerAllowed] to define, which pointer is valid for
// this gesture
@override
void addAllowedPointer(PointerDownEvent event) {
if (_pointer != null) return;
super.addAllowedPointer(event);
// claim tracked pointers
resolve(GestureDisposition.accepted);
_pointer = event.pointer;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
}
// called after pointer was claimed
@override
void acceptGesture(int pointer) {
_checkStart(_startTimestamp!, pointer);
}
// copied from [DragGestureRecognizer]
void _checkStart(Duration timestamp, int pointer) {
if (onStart != null) {
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
globalPosition: _initialPosition.global,
localPosition: _initialPosition.local,
kind: getKindForPointer(pointer),
);
invokeCallback<void>('onStart', () => onStart!(details));
}
}
// copied from [DragGestureRecognizer]
void _checkUpdate({
Duration? sourceTimeStamp,
required Offset delta,
double? primaryDelta,
required Offset globalPosition,
Offset? localPosition,
}) {
if (onUpdate != null) {
final DragUpdateDetails details = DragUpdateDetails(
sourceTimeStamp: sourceTimeStamp,
delta: delta,
primaryDelta: primaryDelta,
globalPosition: globalPosition,
localPosition: localPosition,
);
invokeCallback<void>('onUpdate', () => onUpdate!(details));
}
}
// copied from [DragGestureRecognizer]
void _checkEnd(int pointer) {
if (onEnd != null) {
invokeCallback<void>('onEnd', () => onEnd!(DragEndDetails(primaryVelocity: 0.0)));
}
}
}
This is actually a really nice way of implementing gesture disambiguation! One benefit of using flutter, but some more documentation on how to write gesture recognizers would be great!