Search code examples
flutterdartlistenerstylushittest

Flutter: Filter hitevent for specific PointerDeviceKind and only absorb these


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
    }
  }
}

Solution

  • Turns out, the mechanism I was searching for is built on Listener and is called gesture-disambiguation. Its API is exposed through RawGestureDetector and GestureRecognizers. 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 PointerDeviceKinds. 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 GestureRecognizers 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!