[譯] 深刻 Flutter 之手勢

Flutter 提供了一些很是棒的預製組件,用於處理觸摸事件,如 in InkWellInkResponse。用這些組件包裹住你的組件,它們就可以響應觸摸事件了。除此以外,它還會向你的組件添加 Material 風格的飛濺效果。例如,當從組件的邊界延伸出來時,InkResponse 能夠選擇控制飛濺的形狀和剪裁效果。有趣的是 InkWellInkResponse 不會作任何渲染,而是更新父級的 Material 組件。一個常見的例子是圖片。若是用 inkEll 將圖片包裹起來,你會注意到紋波並不可見。這是由於它是在 Material 上的圖片後面繪製的。想讓 Ink 飛濺效果可見,能夠用 Ink.Image 包裹住圖片。雖然這對大多數任務來講頗有用,但若是你想捕獲更多事件,例如當用戶拖動屏幕時,則應該使用 GestureDetector前端

那麼什麼是手勢探測器?它是如何工做的?

簡單來講手勢檢測器是一個無狀態組件,其構造函數中的參數可用於不一樣的觸摸事件。值得注意的是,你不能同時使用 PanScale,由於 ScalePan 的一個超集。GestureDetector 純粹用於檢測手勢,所以不會給出任何視覺反應(不存在 Material Ink 傳播)。android

下面是一張表格,展現了 GestureDetector 提供的不一樣回調以及對應的簡短描述:ios

屬性/回調 描述
onTapDown 每次用戶與屏幕聯繫時都會觸發 OnTapDown
onTapUp 當用戶中止觸摸屏幕時,onTapUp 被調用。
onTap 當短暫觸摸屏幕時,onTap 被觸發。
onTapCancel 當用戶觸摸屏幕但未完成 Tap 時,將觸發此事件。
onDoubleTap 當屏幕被快速連續觸摸兩次時調用 onDoubleTap
onLongPress 用戶觸摸屏幕超過 500毫秒 時,onLongPress 被觸發。
onVerticalDragDown 當指針與屏幕接觸並開始沿垂直方向移動時,onVerticalDown 被調用。
onVerticalDragStart 當指針 開始 沿垂直方向移動時調用 onVerticalDragStart
onVerticalDragUpdate 每次指針在屏幕上的位置發生變化時都會調用此方法。
onVerticalDragEnd 當用戶中止移動時,拖動被認爲是完成的,將調用此事件。
onVerticalDragCancel 當用戶忽然中止拖動時調用。
onHorizontalDragDown 當用戶/指針與屏幕接觸並開始水平移動時調用。
onHorizontalDragStart 用戶/指針已與屏幕接觸並 開始 沿水平方向移動。
onHorizontalDragUpdate 每次指針在水平方向/x軸上的位置發生變化時調用。
onHorizontalDragEnd 在水平拖動結束時,將調用此事件。
onHorizontalDragCancel 當指針未成功觸發 onHorizontalDragDown 時調用。
onPanDown 當指針與屏幕接觸時調用。
onPanStart 指針事件開始移動時,onPanStart 觸發。
onPanUpdate 每次指針改變位置時,調用 onPanUpdate
onPanEnd 平移完成後,將調用此事件。
onScaleStart 當指針與屏幕接觸並創建 1.0 的焦點時,將調用此事件。
onScaleUpdate 與屏幕接觸的指針指示了新的焦點。
onScaleEnd 當指針再也不與指示手勢結束的屏幕接觸時調用。

GestureDetector 會根據哪一個回調非空來決定嘗試識別哪些手勢。這頗有用,由於若是你須要禁用手勢,則須要傳入 nullgit

讓咱們以 **onTap** 手勢爲例,肯定如何處理 **GestureDetector**github

首先,咱們使用 onTap 回調建立一個 GestureDetector,由於是非 null,當發生 tap 事件時 GestureDetector 會使用咱們的回調。在 GestureDetector 內部,建立了一個 Gesture FactoryGesture Recognizer 會作大量工做來肯定正在處理什麼手勢。這個過程對於 GestureDetector 提供的全部回調來講是相同的。GestureFactories 隨後會被傳遞到 RawGestureDetector後端

RawGestureDetector 會爲檢測手勢作大量工做。它是一個 有狀態組件 ,當狀態改變時會同步全部手勢,處理識別器,獲取發生的全部 指針事件 並將其發送到註冊的識別器。而後它們將在 手勢競技場 中一決雌雄。bash

RawGestureDetectorbuild 構建方法由一個 用於監聽指針事件的基類 Listener 組成。若是你想使用來自平臺的原始輸入,如向上,向下或取消事件,這是你的首選類。Listener 不會給你任何手勢,只有基本的 onPointerDownonPointerUponPointerMoveonPointerCancel 事件。一切都必須手動處理,包括向 手勢競技場 報告本身。若是不這樣作,那麼你不會得到自動取消,也沒法參與那裏發生的交互。這是 組件端 的最底層。併發

Listener 是一個 SingleChildRenderObjectWidget,由繼承自 RenderProxyBoxWithHitTestBehavior 的類 RenderPointerListener 組成的,這意味着它會模仿其子類的屬性,同時容許自定義 HitTestBehavior。若是你想了解渲染盒及其運做方式的更多信息,請閱讀 Norbert Kozsir 撰寫的這篇文章。框架

HitTestBehaviour 有三個選項,deferToChildopaquetranslucent。這些來自 GestureDetector,且能夠在其中進行配置。DeferToChild 將事件沿着組件樹向下傳遞,這也是 默認行爲Opaque 會防止後臺組件接收事件,而 Translucent 則容許後臺組件接收事件。less

那麼若是你但願父組件和子組件都接收指針事件呢?

讓咱們暫時想象一下你有一個嵌套列表的狀況,你想要同時滾動它們。爲此,你須要父組件和子組件都接收到指針。你配置命中測試行爲,使其是半透明的,確保兩個組件都接收到事件,但事情卻不按計劃進行...爲何?

上述問題的答案就是 GestureArena

GestureArena 被用於 手勢消歧 。全部識別器都會在這裏一決雌雄併發送出去。在屏幕上的任何給定點處,能夠存在多個手勢識別器。競技場會考慮用戶觸摸屏幕的時長,斜率以及拖動方向來肯定勝利者。

父列表和子列表都會將其識別器發送到競技場,但(在撰寫本文時)只有一個會贏,並且它剛好老是子列表。

修復方法是使用 GestureFactory 的同時使用 RawGestureDetector 來改變競技場的表現。

舉個例子,讓咱們建立一個由兩個容器組成的簡單應用程序。目標是讓子容器和父容器都接收到手勢。

RawGestureDetector 將兩個容器都包裹起來。接下來,咱們將建立一個自定義手勢識別器 AllowMultipleGestureRecognizerGestureRecognizer 是全部其餘識別器繼承的基類。它爲類提供基礎 API ,以便它們可以與手勢識別器一塊兒工做/交互。值得注意的是,GestureRecognizer 並不關心識別器自己的具體細節。

// 自定義手勢識別器。
// 重寫 rejectGesture()。當一個手勢被拒絕時,將調用此函數。默認狀況下,它會處理
// 識別器並進行清理。可是咱們修改了它,它其實是手動添加的,以代替識別器被處理。
// 結果是你將有兩個識別器在競技場中獲勝。這是共贏。

class AllowMultipleGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}
複製代碼

在上面的代碼中,咱們正在建立一個繼承自 TapGestureRecognizer 的自定義類 AllowMultipleGestureRecognizer。這意味着它可以繼承 TapGestureRecognizer。在這個例子中,咱們重寫了 rejectGesture,使之不是處理識別器,而是手動接受。

如今咱們將 GestureRecognizerFactoryWithHandlers 中的自定義手勢識別器傳遞給 RawGestureDetector

Widget build(BuildContext context) {
   return RawGestureDetector(
     gestures: {
       AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<
          AllowMultipleGestureRecognizer>(
         () => AllowMultipleGestureRecognizer(), //構造函數
         (AllowMultipleGestureRecognizer instance) { //初始化器
           instance.onTap = () => print('Episode 4 is best! (parent container) ');
         },
       )
     },
複製代碼

如今咱們將 GestureRecognizerFactoryWithHandlers 中的自定義手勢識別器傳遞給 RawGestureDetector。工廠函數須要兩個屬性,構造函數和初始化器,用於構造和初始化手勢識別器。咱們使用 lambda 傳遞這些參數。如上面的代碼所述,構造函數返回 AllowMultipleGestureRecognizer 的一個新實例,而初始化器則獲取用於監聽 tap 並將一些文本打印到控制檯的屬性 instance。兩個容器將重複這一過程,惟一的區別是打印的文本。

如下是示例應用的完整源碼:

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

//主函數。 Flutter 應用的入口
void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: DemoApp(),
      ),
    ),
  );
}

//   簡單的演示應用程序,由兩個容器組成。目標是容許多個手勢進入競技場。
//  全部的東西都是經過 `RawGestureDetector` 和自定義 `GestureRecognizer` (繼承自 `TapGestureRecognizer` )
//  將自定義 GestureRecognizer,`AllowMultipleGestureRecognizer` 添加到手勢列表中,並建立一個 `AllowMultipleGestureRecognizer` 類型的 `GestureRecognizerFactoryWithHandlers`。
//  它用給定的回調建立一個手勢識別器工廠函數,在這裏是 `onTap`。
//  它監聽 `onTap` 的一個實例,而後在被調用時向控制檯打印文本。須要注意的是,`RawGestureDetector` 對於兩個容器
//  是相同的。惟一的區別是打印的文本(用來標識組件)。

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<
            AllowMultipleGestureRecognizer>(
          () => AllowMultipleGestureRecognizer(),
          (AllowMultipleGestureRecognizer instance) {
            instance.onTap = () => print('Episode 4 is best! (parent container) ');
          },
        )
      },
      behavior: HitTestBehavior.opaque,
      //父容器
      child: Container(
        color: Colors.blueAccent,
        child: Center(
          //用 RawGestureDetector 將兩個容器包裹起來
          child: RawGestureDetector(
            gestures: {
              AllowMultipleGestureRecognizer:
                  GestureRecognizerFactoryWithHandlers<
                      AllowMultipleGestureRecognizer>(
                () => AllowMultipleGestureRecognizer(),  //構造函數
                (AllowMultipleGestureRecognizer instance) {  //初始化器
                  instance.onTap = () => print('Episode 8 is best! (nested container)');
                },
              )
            },
            //在第一個容器中建立嵌套容器。
            child: Container(
               color: Colors.yellowAccent,
               width: 300.0,
               height: 400.0,
            ),
          ),
        ),
      ),
    );
  }
}

// 自定義手勢識別器。
// 重寫 rejectGesture()。當一個手勢被拒絕時,將調用此函數。默認狀況下,它會處理
// 識別器並進行清理。可是咱們修改了它,它其實是手動添加的,以代替識別器被處理。
// 結果是你將有兩個識別器在競技場中獲勝。這是共贏。
class AllowMultipleGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}
複製代碼

那麼運行上面代碼的結果是什麼?

當你點擊黃色容器時,兩個組件都會收到 tap 事件,所以有兩條語句打印到控制檯。

應用程序:

控制檯輸出:

你贏的時候會發生什麼?

一個手勢獲勝後,競技場將處於 closedswept 狀態。這將丟棄未使用的識別器並重置競技場。而後由勝利手勢執行動做。

回到咱們的 Tap 示例,在此以後,映射到 onTap 的函數如今將被執行。

總結

今天咱們瞭解了 Flutter 框架如何處理手勢。咱們首先了解了 Flutter 爲處理 taps 和其餘觸摸事件提供的夢幻般的預製組件。接下來,咱們討論了 GestureDetector 並實驗了其內部工做方式。經過使用示例,咱們瞭解了 Flutter 如何處理 Tap 手勢。咱們穿過了 RawGestureDetector 這片土地,聆聽了 Listener 的聲音,並向名爲 GestureArena 的神祕的 Flutter 搏擊俱樂部致敬。

最後,咱們從應用程序的角度介紹了 Flutter 中的大部分手勢系統。有了這些知識,你如今應該對如何獲取屏幕上的觸摸並在幕後進行處理有了更好地理解。若是你有任何問題或疑慮,請隨時發表評論或經過 Twitterverse 與我聯繫。

一樣 很是 感謝Simon Lightfoot(又名「Flutter Whisperer」)對本文的貢獻❤

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索