flutter系列之:移動端的手勢基礎GestureDetector

簡介

移動的和PC端有什麼不同呢?同樣的H5可以運行在APP端,也可以運行在PC端。兩者最大的區別就是移動端可以用手勢。手勢可以做到一些比如左滑右滑,上滑下滑,縮放等操作。

原生的andorid和IOS當然可以做到這些事情,作為一個移動的的開發框架flutter,自然也能夠支援手勢。flutter中的手勢支援叫做GestureDetector,一起來看看flutter中的手勢基礎吧。

Pointers和Listener

我們先來考慮一下最簡單的手勢是什麼呢?很明顯,最簡單的手勢就是模擬滑鼠的點擊操作。我們可以將其稱之為Pointer event,也就是各種點擊事件。

flutter中有四種Pointer事件,這些事件如下所示:

  • PointerDownEvent –表示用手點擊了螢幕,接觸到了一個widget。
  • PointerMoveEvent –表示手指從一個位置移動到另外一個位置。
  • PointerUpEvent –手指從點擊螢幕變成了離開螢幕。
  • PointerCancelEvent –表示手指離開了該應用程式。

那麼點擊事件的傳遞機制是什麼樣的呢?

以手指點擊螢幕的PointerDownEvent事件為例,當手指點擊螢幕的時候,flutter首先會去定位該點擊位置存在的widget,然後將該點擊事件傳遞給該位置的最小widget.

然後點擊事件從最新的widget向上開始冒泡,並將其分派到從最裡面的widget到樹根的路徑上的所有widget中。

注意,flutter中並沒有取消或停止進一步分派Pointer事件的機制。

要想監聽這寫Pointer事件,最簡單直接的辦法就是使用Listener:

class Listener extends SingleChildRenderObjectWidget {
  /// Creates a widget that forwards point events to callbacks.
  ///
  /// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
  const Listener({
    Key? key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerUp,
    this.onPointerHover,
    this.onPointerCancel,
    this.onPointerSignal,
    this.behavior = HitTestBehavior.deferToChild,
    Widget? child,
  }) : assert(behavior != null),
       super(key: key, child: child);

可以看到Listener也是一種widget,並且可以監聽多種Pointer的事件。

我們可以把要監聽Pointer的widget封裝在Listener中,這樣就可以監聽各種Pointer事件了,具體的例子如下:

Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.tight(const Size(300.0, 200.0)),
      child: Listener(
        onPointerDown: _incrementDown,
        onPointerMove: _updateLocation,
        onPointerUp: _incrementUp,
        child: Container(
          color: Colors.lightBlueAccent,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                  'You have pressed or released in this area this many times:'),
              Text(
                '$_downCounter presses\n$_upCounter releases',
                style: Theme.of(context).textTheme.headline4,
              ),
              Text(
                'The cursor is here: (${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)})',
              ),
            ],
          ),
        ),
      ),
    );

 void _incrementDown(PointerEvent details) {
    _updateLocation(details);
    setState(() {
      _downCounter++;
    });
  }

  void _incrementUp(PointerEvent details) {
    _updateLocation(details);
    setState(() {
      _upCounter++;
    });
  }

  void _updateLocation(PointerEvent details) {
    setState(() {
      x = details.position.dx;
      y = details.position.dy;
    });
  }

但是對於Lisenter來說只能監聽最原始的Pointer事件,所以如果想監聽更多類型的手勢事件的話,則可以使用GestureDetector.

GestureDetector

GestureDetector可以檢測下面這些手勢,包括:

  1. Tap

Tap表示的是用戶點擊的事件,Tap有下面幾種事件:

onTapDown
onTapUp
onTap
onTapCancel
  1. Double tap

Double tap表示的是雙擊事件,Double tap只有一種類型:

onDoubleTap
  1. Long press

Long press表示的是長按。也只有下面一種類型:

onLongPress
  1. Vertical drag

Vertical drag表示的是垂直方向的拉,它有三個事件,分別是:

onVerticalDragStart
onVerticalDragUpdate
onVerticalDragEnd
  1. Horizontal drag

有垂直方向的拉,就有水平方向的拉,Horizontal drag表示的是水平方向的拉,它同樣有三個事件,分別是:

onHorizontalDragStart
onHorizontalDragUpdate
onHorizontalDragEnd
  1. Pan

Pan這個東西可以看做是Vertical drag和Horizontal drag的合集, 因為有時候我們是希望同時可以水平或者垂直移動,在這種情況下面,我們就需要使用到Pan的事件:

onPanStart
onPanUpdate
onPanEnd

注意, Pan是和單獨的Vertical drag、Horizontal drag是相互衝突的,不能同時使用。

要想監聽上面的這些事件,我們可以使用GestureDetector,先看下GestureDetector的定義:

class GestureDetector extends StatelessWidget {
  GestureDetector({
    Key? key,
    this.child,
    this.onTapDown,
    this.onTapUp,
    this.onTap,
    this.onTapCancel,
    this.onSecondaryTap,
    this.onSecondaryTapDown,
    this.onSecondaryTapUp,
    this.onSecondaryTapCancel,
    this.onTertiaryTapDown,
    this.onTertiaryTapUp,
    this.onTertiaryTapCancel,
    this.onDoubleTapDown,
    this.onDoubleTap,
    this.onDoubleTapCancel,
    this.onLongPressDown,
    this.onLongPressCancel,
    this.onLongPress,
    this.onLongPressStart,
    this.onLongPressMoveUpdate,
    this.onLongPressUp,
    this.onLongPressEnd,
    this.onSecondaryLongPressDown,
    this.onSecondaryLongPressCancel,
    this.onSecondaryLongPress,
    this.onSecondaryLongPressStart,
    this.onSecondaryLongPressMoveUpdate,
    this.onSecondaryLongPressUp,
    this.onSecondaryLongPressEnd,
    this.onTertiaryLongPressDown,
    this.onTertiaryLongPressCancel,
    this.onTertiaryLongPress,
    this.onTertiaryLongPressStart,
    this.onTertiaryLongPressMoveUpdate,
    this.onTertiaryLongPressUp,
    this.onTertiaryLongPressEnd,
    this.onVerticalDragDown,
    this.onVerticalDragStart,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
    this.onVerticalDragCancel,
    this.onHorizontalDragDown,
    this.onHorizontalDragStart,
    this.onHorizontalDragUpdate,
    this.onHorizontalDragEnd,
    this.onHorizontalDragCancel,
    this.onForcePressStart,
    this.onForcePressPeak,
    this.onForcePressUpdate,
    this.onForcePressEnd,
    this.onPanDown,
    this.onPanStart,
    this.onPanUpdate,
    this.onPanEnd,
    this.onPanCancel,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.behavior,
    this.excludeFromSemantics = false,
    this.dragStartBehavior = DragStartBehavior.start,
  })

可以看到GestureDetector是一個無狀態的Widget,它和Listner一樣,可以接受一個child Widget,然後監聽了很多手勢的事件。

所以, 一般來說,我們這樣來使用它:

GestureDetector(
              onTap: () {
                setState(() {
                  // Toggle light when tapped.
                  _lightIsOn = !_lightIsOn;
                });
              },
              child: Container(
                color: Colors.yellow.shade600,
                padding: const EdgeInsets.all(8),
                // Change button text when light changes state.
                child: Text(_lightIsOn ? 'TURN LIGHT OFF' : 'TURN LIGHT ON'),
              ),
            ),

注意, 如果GestureDetector中有child,那麼onTap的作用範圍就在子child的範圍。如果GestureDetector中並沒有child,那麼其作用範圍就是GestureDetector的父widget的範圍。

手勢衝突

因為手勢的監聽有很多種方式,但是這些方式並不是完全獨立的,有時候這些手勢可能是互相衝突的。比如前面我們提到的Pan和Vertical drag、Horizontal drag。

如果遇到這樣的情況,那麼futter會自行進行衝突解決,去選擇到底用戶執行的是哪個操作。

比如,當用戶同時進行水平和垂直拖動的時候,兩個識別器在接收到指針向下事件時都會開始觀察指針移動事件。

如果指針水平移動超過一定數量的邏輯像素,則水平識別器獲勝,然後將該手勢解釋為水平拖動。 類似地,如果用戶垂直移動超過一定數量的邏輯像素,則垂直識別器獲勝。

總結

手勢識別是移動端的優勢項目,大家可以嘗試在需要的地方使用GestureDetector,可以達到意想不到的用戶效果哦。

更多內容請參考 //www.flydean.com/05-flutter-gestures/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!