[譯] 深入 Flutter 之手勢

MeFelixWang發表於2018-08-13

[譯] 深入 Flutter 之手勢

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

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

簡單來說手勢檢測器是一個無狀態元件,其建構函式中的引數可用於不同的觸控事件。值得注意的是,你不能同時使用 PanScale,因為 ScalePan 的一個超集。GestureDetector 純粹用於檢測手勢,因此不會給出任何視覺反應(不存在 Material Ink 傳播)。

下面是一張表格,展示了 GestureDetector 提供的不同回撥以及對應的簡短描述:

屬性/回撥 描述
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 會根據哪個回撥非空來決定嘗試識別哪些手勢。這很有用,因為如果你需要禁用手勢,則需要傳入 null

讓我們以 **onTap** 手勢為例,確定如何處理 **GestureDetector**

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

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

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

Listener 是一個 SingleChildRenderObjectWidget,由繼承自 RenderProxyBoxWithHitTestBehavior 的類 RenderPointerListener 組成的,這意味著它會模仿其子類的屬性,同時允許自定義 HitTestBehavior。如果你想了解渲染盒及其運作方式的更多資訊,請閱讀 Norbert Kozsir 撰寫的這篇文章。

HitTestBehaviour 有三個選項,deferToChildopaquetranslucent。這些來自 GestureDetector,且可以在其中進行配置。DeferToChild 將事件沿著元件樹向下傳遞,這也是 預設行為Opaque 會防止後臺元件接收事件,而 Translucent 則允許後臺元件接收事件。

那麼如果你希望父元件和子元件都接收指標事件呢?

讓我們暫時想象一下你有一個巢狀列表的情況,你想要同時滾動它們。為此,你需要父元件和子元件都接收到指標。你配置命中測試行為,使其是半透明的,確保兩個元件都接收到事件,但事情卻不按計劃進行...為什麼?

上述問題的答案就是 GestureArena

[譯] 深入 Flutter 之手勢

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 事件,因此有兩條語句列印到控制檯。

應用程式:

[譯] 深入 Flutter 之手勢

控制檯輸出:

[譯] 深入 Flutter 之手勢

你贏的時候會發生什麼?

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

回到我們的 Tap 示例,在此之後,對映到 onTap 的函式現在將被執行。

總結

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

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

同樣 非常 感謝Simon Lightfoot(又名“Flutter Whisperer”)對本文的貢獻❤

[譯] 深入 Flutter 之手勢

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


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

相關文章