手勢偵測器


原始指標事件〉中看過,透過 Listerner 只能處理基本的指標事件,如果想處理拖曳、雙連觸(Double tap)、縮放等事件,Flutter 中提供了 GestureDetector,它基於原始指標事件,提供了各種手勢識別,最簡單的用法之一,在〈按鈕!按鈕?〉曾經看過:

...
class TextButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;

  TextButton(this.text, {this.onPressed});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Text(text),
      onTap: onPressed == null ? () => {} : onPressed,
    );
  }
}

GestureDetector 可以傾聽的事件很多,逐一介紹實在沒什麼意思,如果想有個出發點,〈Flutter. GestureDetector in-depth〉中一些操作示範與簡單程式碼可以參考。

手勢偵測的實現也是蠻複雜的,若想要能從原始碼中瞭解一下實現原理,需要有一些出發點,這邊就來稍微看一下 GestureDetector 的實現好了,就從它的 build 方法來看看:

  ...
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (
      onTapDown != null ||
      onTapUp != null ||
      onTap != null ||
      onTapCancel != null ||
      onSecondaryTapDown != null ||
      onSecondaryTapUp != null ||
      onSecondaryTapCancel != null
    ) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel
            ..onSecondaryTapDown = onSecondaryTapDown
            ..onSecondaryTapUp = onSecondaryTapUp
            ..onSecondaryTapCancel = onSecondaryTapCancel;
        },
      );
    }

    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance.onDoubleTap = onDoubleTap;
        },
      );
    }


    略...

    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
  ...

方才談到,GestureDetector 基於原始指標事件,提供了各種手勢識別,不同的手勢識別,是定義在不同的 GestureRecognizer,在 build 中就可以看到,依指定要傾聽的事件而定,會加入 TapGestureRecognizerDoubleTapGestureRecognizer 等的工廠類別實例。

GestureRecognizer 最重要是幾個接受 PointerDownEvent 的方法:

abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
  ...略

  void addPointer(PointerDownEvent event) {
    _pointerToKind[event.pointer] = event.kind;
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }

  @protected
  void addAllowedPointer(PointerDownEvent event) { }

  @protected
  void handleNonAllowedPointer(PointerDownEvent event) { }

  @protected
  bool isPointerAllowed(PointerDownEvent event) {
    return _kindFilter == null || _kindFilter == event.kind;
  }

  ...略
}

GestureRecognizer 的子類要實現的,就是識別這些原始指標事件各自代表著什麼手勢,有些手勢可能會有競爭問題,例如在拖曳時,你可能傾聽了垂直與水平拖曳事件,那麼最後會是觸發哪個事件呢?GestureRecognizer 實例必須競爭,就像是在競技場(Arena)中,每個參與競爭的實例就是個 GestureArenaMember,最後只能有一個勝出。

GestureRecognizer 最後用 gestures 作為 RawGestureDetector 建構式的引數,而 RawGestureDetector 是個 StatefulWidget,對應 Statebuild 方法實現是:

  ...略
  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (final GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
  }

  ...略
  @override
  Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child,
    );
    if (!widget.excludeFromSemantics)
      result = _GestureSemantics(
        child: result,
        assignSemantics: _updateSemanticsForRenderObject,
      );
    return result;
  }
  ...略

這邊可以看到建立了 Listener 實例,而 onPointerDown 事件的處理器 _handlePointerDown,其中取得每個 GestureRecognizer,將指標事件 PointerDownEvent 指定給 addPointer

因此想要研讀一下手勢實現的相關原始碼的話,可以從 GestureDetectorGestureRecognizerRawGestureDetector 開始著手,而看過以上的簡單原始碼後,應該也可以知道,GestureDetector 只是個空殼,若想直接使用 RawGestureDetector 也是可以的:

import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';

void main() => runApp(
    MaterialApp(
      home: Scaffold(
          body: Center(
              child: TextButton('按我',
                onPressed: () => print('被按了XD'),
              )
          )
      ),
    )
);

class TextButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;

  TextButton(this.text, {this.onPressed});

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
      TapGestureRecognizer : GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
            () => TapGestureRecognizer(debugOwner: this),
            (TapGestureRecognizer instance) => instance.onTap = onPressed,
      )
    };

    return RawGestureDetector(
      gestures: gestures,
      child: Text(text),
    );
  }
}