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

2021-03-08 14:15 更新

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

#8.2.1 GestureDetector

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

#點擊、雙擊、長按

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

  1. class GestureDetectorTestRoute extends StatefulWidget {
  2. @override
  3. _GestureDetectorTestRouteState createState() =>
  4. new _GestureDetectorTestRouteState();
  5. }
  6. class _GestureDetectorTestRouteState extends State<GestureDetectorTestRoute> {
  7. String _operation = "No Gesture detected!"; //保存事件名
  8. @override
  9. Widget build(BuildContext context) {
  10. return Center(
  11. child: GestureDetector(
  12. child: Container(
  13. alignment: Alignment.center,
  14. color: Colors.blue,
  15. width: 200.0,
  16. height: 100.0,
  17. child: Text(_operation,
  18. style: TextStyle(color: Colors.white),
  19. ),
  20. ),
  21. onTap: () => updateText("Tap"),//點擊
  22. onDoubleTap: () => updateText("DoubleTap"), //雙擊
  23. onLongPress: () => updateText("LongPress"), //長按
  24. ),
  25. );
  26. }
  27. void updateText(String text) {
  28. //更新顯示的事件名
  29. setState(() {
  30. _operation = text;
  31. });
  32. }
  33. }

運行效果如圖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的示例:

  1. class _Drag extends StatefulWidget {
  2. @override
  3. _DragState createState() => new _DragState();
  4. }
  5. class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
  6. double _top = 0.0; //距頂部的偏移
  7. double _left = 0.0;//距左邊的偏移
  8. @override
  9. Widget build(BuildContext context) {
  10. return Stack(
  11. children: <Widget>[
  12. Positioned(
  13. top: _top,
  14. left: _left,
  15. child: GestureDetector(
  16. child: CircleAvatar(child: Text("A")),
  17. //手指按下時會觸發(fā)此回調(diào)
  18. onPanDown: (DragDownDetails e) {
  19. //打印手指按下的位置(相對于屏幕)
  20. print("用戶手指按下:${e.globalPosition}");
  21. },
  22. //手指滑動時會觸發(fā)此回調(diào)
  23. onPanUpdate: (DragUpdateDetails e) {
  24. //用戶手指滑動時,更新偏移,重新構(gòu)建
  25. setState(() {
  26. _left += e.delta.dx;
  27. _top += e.delta.dy;
  28. });
  29. },
  30. onPanEnd: (DragEndDetails e){
  31. //打印滑動結(jié)束時在x、y軸上的速度
  32. print(e.velocity);
  33. },
  34. ),
  35. )
  36. ],
  37. );
  38. }
  39. }

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

圖8-3

日志:

  1. I/flutter ( 8513): 用戶手指按下:Offset(26.3, 101.8)
  2. 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可以只識別特定方向的手勢事件,我們將上面的例子改為只能沿垂直方向拖動:

  1. class _DragVertical extends StatefulWidget {
  2. @override
  3. _DragVerticalState createState() => new _DragVerticalState();
  4. }
  5. class _DragVerticalState extends State<_DragVertical> {
  6. double _top = 0.0;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Stack(
  10. children: <Widget>[
  11. Positioned(
  12. top: _top,
  13. child: GestureDetector(
  14. child: CircleAvatar(child: Text("A")),
  15. //垂直方向拖動事件
  16. onVerticalDragUpdate: (DragUpdateDetails details) {
  17. setState(() {
  18. _top += details.delta.dy;
  19. });
  20. }
  21. ),
  22. )
  23. ],
  24. );
  25. }
  26. }

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

#縮放

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

  1. class _ScaleTestRouteState extends State<_ScaleTestRoute> {
  2. double _width = 200.0; //通過修改圖片寬度來達到縮放效果
  3. @override
  4. Widget build(BuildContext context) {
  5. return Center(
  6. child: GestureDetector(
  7. //指定寬度,高度自適應(yīng)
  8. child: Image.asset("./images/sea.png", width: _width),
  9. onScaleUpdate: (ScaleUpdateDetails details) {
  10. setState(() {
  11. //縮放倍數(shù)在0.8到10倍之間
  12. _width=200*details.scale.clamp(.8, 10.0);
  13. });
  14. },
  15. ),
  16. );
  17. }
  18. }

運行效果如圖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è)我們需要在點擊時給文本變色:

  1. import 'package:flutter/gestures.dart';
  2. class _GestureRecognizerTestRouteState
  3. extends State<_GestureRecognizerTestRoute> {
  4. TapGestureRecognizer _tapGestureRecognizer = new TapGestureRecognizer();
  5. bool _toggle = false; //變色開關(guān)
  6. @override
  7. void dispose() {
  8. //用到GestureRecognizer的話一定要調(diào)用其dispose方法釋放資源
  9. _tapGestureRecognizer.dispose();
  10. super.dispose();
  11. }
  12. @override
  13. Widget build(BuildContext context) {
  14. return Center(
  15. child: Text.rich(
  16. TextSpan(
  17. children: [
  18. TextSpan(text: "你好世界"),
  19. TextSpan(
  20. text: "點我變色",
  21. style: TextStyle(
  22. fontSize: 30.0,
  23. color: _toggle ? Colors.blue : Colors.red
  24. ),
  25. recognizer: _tapGestureRecognizer
  26. ..onTap = () {
  27. setState(() {
  28. _toggle = !_toggle;
  29. });
  30. },
  31. ),
  32. TextSpan(text: "你好世界"),
  33. ]
  34. )
  35. ),
  36. );
  37. }
  38. }

運行效果:

圖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é)束都會沿著該方向移動。代碼如下:

  1. import 'package:flutter/material.dart';
  2. class BothDirectionTestRoute extends StatefulWidget {
  3. @override
  4. BothDirectionTestRouteState createState() =>
  5. new BothDirectionTestRouteState();
  6. }
  7. class BothDirectionTestRouteState extends State<BothDirectionTestRoute> {
  8. double _top = 0.0;
  9. double _left = 0.0;
  10. @override
  11. Widget build(BuildContext context) {
  12. return Stack(
  13. children: <Widget>[
  14. Positioned(
  15. top: _top,
  16. left: _left,
  17. child: GestureDetector(
  18. child: CircleAvatar(child: Text("A")),
  19. //垂直方向拖動事件
  20. onVerticalDragUpdate: (DragUpdateDetails details) {
  21. setState(() {
  22. _top += details.delta.dy;
  23. });
  24. },
  25. onHorizontalDragUpdate: (DragUpdateDetails details) {
  26. setState(() {
  27. _left += details.delta.dx;
  28. });
  29. },
  30. ),
  31. )
  32. ],
  33. );
  34. }
  35. }

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

#手勢沖突

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

  1. class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  2. double _left = 0.0;
  3. @override
  4. Widget build(BuildContext context) {
  5. return Stack(
  6. children: <Widget>[
  7. Positioned(
  8. left: _left,
  9. child: GestureDetector(
  10. child: CircleAvatar(child: Text("A")), //要拖動和點擊的widget
  11. onHorizontalDragUpdate: (DragUpdateDetails details) {
  12. setState(() {
  13. _left += details.delta.dx;
  14. });
  15. },
  16. onHorizontalDragEnd: (details){
  17. print("onHorizontalDragEnd");
  18. },
  19. onTapDown: (details){
  20. print("down");
  21. },
  22. onTapUp: (details){
  23. print("up");
  24. },
  25. ),
  26. )
  27. ],
  28. );
  29. }
  30. }

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

  1. I/flutter (17539): down
  2. 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)聽原始指針事件就行:

  1. Positioned(
  2. top:80.0,
  3. left: _leftB,
  4. child: Listener(
  5. onPointerDown: (details) {
  6. print("down");
  7. },
  8. onPointerUp: (details) {
  9. //會觸發(fā)
  10. print("up");
  11. },
  12. child: GestureDetector(
  13. child: CircleAvatar(child: Text("B")),
  14. onHorizontalDragUpdate: (DragUpdateDetails details) {
  15. setState(() {
  16. _leftB += details.delta.dx;
  17. });
  18. },
  19. onHorizontalDragEnd: (details) {
  20. print("onHorizontalDragEnd");
  21. },
  22. ),
  23. ),
  24. )

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號