Flutter實戰(zhàn) 手勢識別

2021-03-08 14:15 更新

本節(jié)先介紹一些 Flutter 中用于處理手勢的GestureDetectorGestureRecognizer,然后再仔細討論一下手勢競爭與沖突問題。

#8.2.1 GestureDetector

GestureDetector是一個用于手勢識別的功能性組件,我們通過它可以來識別各種手勢。GestureDetector實際上是指針事件的語義化封裝,接下來我們詳細介紹一下各種手勢識別。

#點擊、雙擊、長按

我們通過GestureDetectorContainer進行手勢識別,觸發(fā)相應(yīng)事件后,在Container上顯示事件名,為了增大點擊區(qū)域,將Container設(shè)置為 200×100,代碼如下:

class GestureDetectorTestRoute extends StatefulWidget {
  @override
  _GestureDetectorTestRouteState createState() =>
      new _GestureDetectorTestRouteState();
}


class _GestureDetectorTestRouteState extends State<GestureDetectorTestRoute> {
  String _operation = "No Gesture detected!"; //保存事件名
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: Container(
          alignment: Alignment.center,
          color: Colors.blue,
          width: 200.0, 
          height: 100.0,
          child: Text(_operation,
            style: TextStyle(color: Colors.white),
          ),
        ),
        onTap: () => updateText("Tap"),//點擊
        onDoubleTap: () => updateText("DoubleTap"), //雙擊
        onLongPress: () => updateText("LongPress"), //長按
      ),
    );
  }


  void updateText(String text) {
    //更新顯示的事件名
    setState(() {
      _operation = text;
    });
  }
}

運行效果如圖8-2所示:

圖8-2

注意: 當(dāng)同時監(jiān)聽onTaponDoubleTap事件時,當(dāng)用戶觸 發(fā)tap 事件時,會有200毫秒左右的延時,這是因為當(dāng)用戶點擊完之后很可能會再次點擊以觸發(fā)雙擊事件,所以GestureDetector會等一段時間來確定是否為雙擊事件。如果用戶只監(jiān)聽了onTap(沒有監(jiān)聽onDoubleTap)事件時,則沒有延時。

#拖動、滑動

一次完整的手勢過程是指用戶手指按下到抬起的整個過程,期間,用戶按下手指后可能會移動,也可能不會移動。GestureDetector對于拖動和滑動事件是沒有區(qū)分的,他們本質(zhì)上是一樣的。GestureDetector會將要監(jiān)聽的組件的原點(左上角)作為本次手勢的原點,當(dāng)用戶在監(jiān)聽的組件上按下手指時,手勢識別就會開始。下面我們看一個拖動圓形字母A的示例:

class _Drag extends StatefulWidget {
  @override
  _DragState createState() => new _DragState();
}


class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
  double _top = 0.0; //距頂部的偏移
  double _left = 0.0;//距左邊的偏移


  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //手指按下時會觸發(fā)此回調(diào)
            onPanDown: (DragDownDetails e) {
              //打印手指按下的位置(相對于屏幕)
              print("用戶手指按下:${e.globalPosition}");
            },
            //手指滑動時會觸發(fā)此回調(diào)
            onPanUpdate: (DragUpdateDetails e) {
              //用戶手指滑動時,更新偏移,重新構(gòu)建
              setState(() {
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
            onPanEnd: (DragEndDetails e){
              //打印滑動結(jié)束時在x、y軸上的速度
              print(e.velocity);
            },
          ),
        )
      ],
    );
  }
}

運行后,就可以在任意方向拖動了,運行效果如圖8-3所示:

圖8-3

日志:

I/flutter ( 8513): 用戶手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)

代碼解釋:

  • DragDownDetails.globalPosition:當(dāng)用戶按下時,此屬性為用戶按下的位置相對于屏幕(而非父組件)原點(左上角)的偏移。
  • DragUpdateDetails.delta:當(dāng)用戶在屏幕上滑動時,會觸發(fā)多次 Update 事件,delta指一次 Update 事件的滑動的偏移量。
  • DragEndDetails.velocity:該屬性代表用戶抬起手指時的滑動速度(包含 x、y 兩個軸的),示例中并沒有處理手指抬起時的速度,常見的效果是根據(jù)用戶抬起手指時的速度做一個減速動畫。

#單一方向拖動

在本示例中,是可以朝任意方向拖動的,但是在很多場景,我們只需要沿一個方向來拖動,如一個垂直方向的列表,GestureDetector可以只識別特定方向的手勢事件,我們將上面的例子改為只能沿垂直方向拖動:

class _DragVertical extends StatefulWidget {
  @override
  _DragVerticalState createState() => new _DragVerticalState();
}


class _DragVerticalState extends State<_DragVertical> {
  double _top = 0.0;


  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖動事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            }
          ),
        )
      ],
    );
  }
}

這樣就只能在垂直方向拖動了,如果只想在水平方向滑動同理。

#縮放

GestureDetector可以監(jiān)聽縮放事件,下面示例演示了一個簡單的圖片縮放效果:

class _ScaleTestRouteState extends State<_ScaleTestRoute> {
  double _width = 200.0; //通過修改圖片寬度來達到縮放效果


  @override
  Widget build(BuildContext context) {
   return Center(
     child: GestureDetector(
        //指定寬度,高度自適應(yīng)
        child: Image.asset("./images/sea.png", width: _width),
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            //縮放倍數(shù)在0.8到10倍之間
            _width=200*details.scale.clamp(.8, 10.0);
          });
        },
      ),
   );
  }
}

運行效果如圖8-4所示:

圖8-4

現(xiàn)在在圖片上雙指張開、收縮就可以放大、縮小圖片。本示例比較簡單,實際中我們通常還需要一些其它功能,如雙擊放大或縮小一定倍數(shù)、雙指張開離開屏幕時執(zhí)行一個減速放大動畫等,讀者可以在學(xué)習(xí)完后面“動畫”一章中的內(nèi)容后自己來嘗試實現(xiàn)一下。

#8.2.2 GestureRecognizer

GestureDetector內(nèi)部是使用一個或多個GestureRecognizer來識別各種手勢的,而GestureRecognizer的作用就是通過Listener來將原始指針事件轉(zhuǎn)換為語義手勢,GestureDetector直接可以接收一個子 widget。GestureRecognizer是一個抽象類,一種手勢的識別器對應(yīng)一個GestureRecognizer的子類,F(xiàn)lutter 實現(xiàn)了豐富的手勢識別器,我們可以直接使用。

#示例

假設(shè)我們要給一段富文本(RichText)的不同部分分別添加點擊事件處理器,但是TextSpan并不是一個 widget,這時我們不能用GestureDetector,但TextSpan有一個recognizer屬性,它可以接收一個GestureRecognizer

假設(shè)我們需要在點擊時給文本變色:

import 'package:flutter/gestures.dart';


class _GestureRecognizerTestRouteState
    extends State<_GestureRecognizerTestRoute> {
  TapGestureRecognizer _tapGestureRecognizer = new TapGestureRecognizer();
  bool _toggle = false; //變色開關(guān)


  @override
  void dispose() {
     //用到GestureRecognizer的話一定要調(diào)用其dispose方法釋放資源
    _tapGestureRecognizer.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text.rich(
          TextSpan(
              children: [
                TextSpan(text: "你好世界"),
                TextSpan(
                  text: "點我變色",
                  style: TextStyle(
                      fontSize: 30.0,
                      color: _toggle ? Colors.blue : Colors.red
                  ),
                  recognizer: _tapGestureRecognizer
                    ..onTap = () {
                      setState(() {
                        _toggle = !_toggle;
                      });
                    },
                ),
                TextSpan(text: "你好世界"),
              ]
          )
      ),
    );
  }
}

運行效果:

圖8-5

注意:使用GestureRecognizer后一定要調(diào)用其dispose()方法來釋放資源(主要是取消內(nèi)部的計時器)。

#8.2.3 手勢競爭與沖突

#競爭

如果在上例中我們同時監(jiān)聽水平和垂直方向的拖動事件,那么我們斜著拖動時哪個方向會生效?實際上取決于第一次移動時兩個軸上的位移分量,哪個軸的大,哪個軸在本次滑動事件競爭中就勝出。實際上 Flutter 中的手勢識別引入了一個 Arena 的概念,Arena 直譯為“競技場”的意思,每一個手勢識別器(GestureRecognizer)都是一個“競爭者”(GestureArenaMember),當(dāng)發(fā)生滑動事件時,他們都要在“競技場”去競爭本次事件的處理權(quán),而最終只有一個“競爭者”會勝出(win)。例如,假設(shè)有一個ListView,它的第一個子組件也是ListView,如果現(xiàn)在滑動這個子ListView,父ListView會動嗎?答案是否定的,這時只有子ListView會動,因為這時子ListView會勝出而獲得滑動事件的處理權(quán)。

#示例

我們以拖動手勢為例,同時識別水平和垂直方向的拖動手勢,當(dāng)用戶按下手指時就會觸發(fā)競爭(水平方向和垂直方向),一旦某個方向“獲勝”,則直到當(dāng)次拖動手勢結(jié)束都會沿著該方向移動。代碼如下:

import 'package:flutter/material.dart';


class BothDirectionTestRoute extends StatefulWidget {
  @override
  BothDirectionTestRouteState createState() =>
      new BothDirectionTestRouteState();
}


class BothDirectionTestRouteState extends State<BothDirectionTestRoute> {
  double _top = 0.0;
  double _left = 0.0;


  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖動事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
            onHorizontalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _left += details.delta.dx;
              });
            },
          ),
        )
      ],
    );
  }
}

此示例運行后,每次拖動只會沿一個方向移動(水平或垂直),而競爭發(fā)生在手指按下后首次移動(move)時,此例中具體的“獲勝”條件是:首次移動時的位移在水平和垂直方向上的分量大的一個獲勝。

#手勢沖突

由于手勢競爭最終只有一個勝出者,所以,當(dāng)有多個手勢識別器時,可能會產(chǎn)生沖突。假設(shè)有一個 widget,它可以左右拖動,現(xiàn)在我們也想檢測在它上面手指按下和抬起的事件,代碼如下:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  double _left = 0.0;
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          left: _left,
          child: GestureDetector(
              child: CircleAvatar(child: Text("A")), //要拖動和點擊的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
                print("down");
              },
              onTapUp: (details){
                print("up");
              },
          ),
        )
      ],
    );
  }
}

現(xiàn)在我們按住圓形“A”拖動然后抬起手指,控制臺日志如下:

I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd

我們發(fā)現(xiàn)沒有打印"up",這是因為在拖動時,剛開始按下手指時在沒有移動時,拖動手勢還沒有完整的語義,此時 TapDown 手勢勝出(win),此時打印"down",而拖動時,拖動手勢會勝出,當(dāng)手指抬起時,onHorizontalDragEndonTapUp發(fā)生了沖突,但是因為是在拖動的語義中,所以onHorizontalDragEnd勝出,所以就會打印 “onHorizontalDragEnd”。如果我們的代碼邏輯中,對于手指按下和抬起是強依賴的,比如在一個輪播圖組件中,我們希望手指按下時,暫停輪播,而抬起時恢復(fù)輪播,但是由于輪播圖組件中本身可能已經(jīng)處理了拖動手勢(支持手動滑動切換),甚至可能也支持了縮放手勢,這時我們?nèi)绻谕獠吭儆?code>onTapDown、onTapUp來監(jiān)聽的話是不行的。這時我們應(yīng)該怎么做?其實很簡單,通過 Listener 監(jiān)聽原始指針事件就行:

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
      print("down");
    },
    onPointerUp: (details) {
      //會觸發(fā)
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("B")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

手勢沖突只是手勢級別的,而手勢是對原始指針的語義化的識別,所以在遇到復(fù)雜的沖突場景時,都可以通過Listener直接識別原始指針事件來解決沖突。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號