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

2021-03-08 14:15 更新

本節(jié)先介紹一些 Flutter 中用于處理手勢(shì)的GestureDetectorGestureRecognizer,然后再仔細(xì)討論一下手勢(shì)競(jìng)爭(zhēng)與沖突問(wèn)題。

#8.2.1 GestureDetector

GestureDetector是一個(gè)用于手勢(shì)識(shí)別的功能性組件,我們通過(guò)它可以來(lái)識(shí)別各種手勢(shì)。GestureDetector實(shí)際上是指針事件的語(yǔ)義化封裝,接下來(lái)我們?cè)敿?xì)介紹一下各種手勢(shì)識(shí)別。

#點(diǎn)擊、雙擊、長(zhǎng)按

我們通過(guò)GestureDetector對(duì)Container進(jìn)行手勢(shì)識(shí)別,觸發(fā)相應(yīng)事件后,在Container上顯示事件名,為了增大點(diǎn)擊區(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"),//點(diǎn)擊
        onDoubleTap: () => updateText("DoubleTap"), //雙擊
        onLongPress: () => updateText("LongPress"), //長(zhǎng)按
      ),
    );
  }


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

運(yùn)行效果如圖8-2所示:

圖8-2

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

#拖動(dòng)、滑動(dòng)

一次完整的手勢(shì)過(guò)程是指用戶手指按下到抬起的整個(gè)過(guò)程,期間,用戶按下手指后可能會(huì)移動(dòng),也可能不會(huì)移動(dòng)。GestureDetector對(duì)于拖動(dòng)和滑動(dòng)事件是沒(méi)有區(qū)分的,他們本質(zhì)上是一樣的。GestureDetector會(huì)將要監(jiān)聽的組件的原點(diǎn)(左上角)作為本次手勢(shì)的原點(diǎn),當(dāng)用戶在監(jiān)聽的組件上按下手指時(shí),手勢(shì)識(shí)別就會(huì)開始。下面我們看一個(gè)拖動(dòng)圓形字母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")),
            //手指按下時(shí)會(huì)觸發(fā)此回調(diào)
            onPanDown: (DragDownDetails e) {
              //打印手指按下的位置(相對(duì)于屏幕)
              print("用戶手指按下:${e.globalPosition}");
            },
            //手指滑動(dòng)時(shí)會(huì)觸發(fā)此回調(diào)
            onPanUpdate: (DragUpdateDetails e) {
              //用戶手指滑動(dòng)時(shí),更新偏移,重新構(gòu)建
              setState(() {
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
            onPanEnd: (DragEndDetails e){
              //打印滑動(dòng)結(jié)束時(shí)在x、y軸上的速度
              print(e.velocity);
            },
          ),
        )
      ],
    );
  }
}

運(yùn)行后,就可以在任意方向拖動(dòng)了,運(yùn)行效果如圖8-3所示:

圖8-3

日志:

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

代碼解釋:

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

#單一方向拖動(dòng)

在本示例中,是可以朝任意方向拖動(dòng)的,但是在很多場(chǎng)景,我們只需要沿一個(gè)方向來(lái)拖動(dòng),如一個(gè)垂直方向的列表,GestureDetector可以只識(shí)別特定方向的手勢(shì)事件,我們將上面的例子改為只能沿垂直方向拖動(dòng):

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")),
            //垂直方向拖動(dòng)事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            }
          ),
        )
      ],
    );
  }
}

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

#縮放

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

class _ScaleTestRouteState extends State<_ScaleTestRoute> {
  double _width = 200.0; //通過(guò)修改圖片寬度來(lái)達(dá)到縮放效果


  @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);
          });
        },
      ),
   );
  }
}

運(yùn)行效果如圖8-4所示:

圖8-4

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

#8.2.2 GestureRecognizer

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

#示例

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

假設(shè)我們需要在點(diǎn)擊時(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: "點(diǎn)我變色",
                  style: TextStyle(
                      fontSize: 30.0,
                      color: _toggle ? Colors.blue : Colors.red
                  ),
                  recognizer: _tapGestureRecognizer
                    ..onTap = () {
                      setState(() {
                        _toggle = !_toggle;
                      });
                    },
                ),
                TextSpan(text: "你好世界"),
              ]
          )
      ),
    );
  }
}

運(yùn)行效果:

圖8-5

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

#8.2.3 手勢(shì)競(jìng)爭(zhēng)與沖突

#競(jìng)爭(zhēng)

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

#示例

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

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")),
            //垂直方向拖動(dòng)事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
            onHorizontalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _left += details.delta.dx;
              });
            },
          ),
        )
      ],
    );
  }
}

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

#手勢(shì)沖突

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

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")), //要拖動(dòng)和點(diǎn)擊的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
                print("down");
              },
              onTapUp: (details){
                print("up");
              },
          ),
        )
      ],
    );
  }
}

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

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

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

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

手勢(shì)沖突只是手勢(shì)級(jí)別的,而手勢(shì)是對(duì)原始指針的語(yǔ)義化的識(shí)別,所以在遇到復(fù)雜的沖突場(chǎng)景時(shí),都可以通過(guò)Listener直接識(shí)別原始指針事件來(lái)解決沖突。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)