深刻理解Flutter的Listener組件

引言

有過移動端開發經驗的同窗都知道,移動端的觸摸事件是由手指按下、手指移動、手指擡起這些基本事件組成的。bash

Flutter中,一切皆WidgetWidget自己並不具有識別觸摸事件的功能。能識別觸摸事件的Widget,必須經由ListenerGestureDetector組裝起來。markdown

GestureDetector本質上仍是由Listener組成的,因此咱們先認識一下Listener函數

Listener

Listener在功能劃分上屬於功能型Widget,主要提供原始觸摸事件的監聽。下面看一下它的構造函數:測試

const Listener({
    Key key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerEnter,
    this.onPointerExit,
    this.onPointerHover,
    this.onPointerUp,
    this.onPointerCancel,
    this.onPointerSignal,
    this.behavior = HitTestBehavior.deferToChild,
    Widget child,
 })
複製代碼

從構造函數中能夠知道,Listener提供了多種觸摸事件的監聽,但咱們常常用到的是onPointerDownonPointerMoveonPointerUp,分別對應手指按下手指移動手指擡起這三個觸摸事件。this

child屬性表示被包裝的Widgetspa

behavior屬性,這是Listener很重要的一個屬性,也是本節着重討論的,可是如今還輪不到他出場,在理解behavior屬性以前,咱們必需要認識一個概念,叫作命中測試(Hit Test)code

1、命中測試

當手指按下、移動或者擡起時,Flutter會給每個事件新建一個對象,如按下是PointerDownEvent,移動是PointerMoveEvent,擡起是PointerUpEvent。對於每個事件對象,Flutter都會執行命中測試,它經歷瞭如下這幾步:orm

一、從最底層的Widget開始執行命中測試,是否命中取決於hitTestChildren方法(它的children Widget是否命中測試)或hitTestSelf方法是否返回true對象

二、循環最底層Widgetchildren Widget,分別執行child Widget的命中測試。child Widget是否命中也取決於hitTestChidren方法(它的children Widget是否命中測試)或hitTestSelf方法是否返回true繼承

三、從下往上遞歸地執行命中測試,直到找到最上層的一個命中測試的Widget,將它加入命中測試列表。因爲它已命中測試,那麼它的父Widget也命中了測試,將父Widget也加入命中測試列表。以此類推,直到將全部命中測試的Widget加入命中測試列表。

舉個例子

爲了更加形象的理解命中測試這個概念,咱們看一下下面的例子。

Listener(
    child: ConstrainedBox(
      constraints: BoxConstraints.tight(Size(200, 200)),
      child: Center(
        child: Text('click me'),
      )
    ),
    onPointerDown: (event) => print("onPointerDown")
)
複製代碼

它的展現效果如上圖所示。

Flutter中,每個Widget實際上會對應一個RenderObject。對於上面代碼來講,上圖爲WidgetRenderObject的對應關係。

一、當點擊了Text時,它的命中測試列表是這樣的: RenderParagraph->RenderPositionedBox->RenderConstrainedBox->RenderPointerListener,因此RenderPointerListenerhandleEvent方法會被執行,最終在控制檯會打印onPointerDown

注意:觸摸事件會循環命中測試列表,並分別執行它們的handleEvent方法。Flutter中幾乎全部Widget對應的RenderObject都是直接或者間接繼承自RenderBox,而RenderBox繼承了HitTestTarget,並重寫了handleEvent方法。

二、當點擊了Text之外的區域時,它的命中測試列表就沒有RenderPointerListener了。爲何呢???

Text之外的區域是ConstrainedBox的(爲何不是Center,由於Center的功能是幫助Text定位,它的區域和Text是一致的)。那ConstrainedBox對應的RenderConstrainedBox命中測試了麼?很顯然是沒有的。

由於ConstrainedBox只有一個child,就是CenterCenter對應的RenderPositionedBox沒有命中測試,致使RenderConstrainedBoxhitTestChildren返回false,而它的hitTestSelf也返回false,因此RenderConstrainedBox沒有命中測試。

Listener也只有一個child,那就是ConstrainedBox,既然RenderConstrainedBox沒有命中測試,那麼RenderPointerListener相應的就沒有命中測試,因此命中測試列表中是沒有RenderPointerListener的。

因此控制檯並不會打印onPointerDown

說明:命中測試方法是RenderBoxRenderObject的子類)的hitTest方法。

上面的例子使用的behavior屬性是默認的HitTestBehavior.deferToChild,若是修改一下behavior屬性會有什麼奇妙的效果呢?

2、behavior屬性

behavior表示命中測試(Hit Test)過程當中的表現策略。它是一個枚舉,提供了三個值,分別是HitTestBehavior.deferToChildHitTestBehavior.opaqueHitTestBehavior.translucent

上面說到過,命中測試,就是看RenderBoxhitTest的返回值,如ListenerhitTest方法以下。

bool hitTest(BoxHitTestResult result, { Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
}

bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
複製代碼

HitTestBehavior.deferToChildListener是否命中測試,取決於子child是否命中測試,這是默認behavior的默認值。

HitTestBehavior.opaque:當Listener的子child沒有命中測試時,該屬性值保證hitTestSelf返回true,即保證Listener所在區域能響應觸摸事件。

HitTestBehavior.translucent:當Listener的子child沒有命中測試時,而且hitTestSelf返回false時,該屬性值能夠保證Listener所在的區域能響應觸摸事件(加入到命中測試列表),可是hitTest方法返回值仍是false,這不能改變。

舉個例子

上面那個例子,咱們將Listenerbehavior屬性修改成HitTestBehavior.opaque

Listener(
    child: ConstrainedBox(
      constraints: BoxConstraints.tight(Size(200, 200)),
      child: Center(
        child: Text('click me'),
      )
    ),
    behavior: HitTestBehavior.opaque, //顯性的修改behavior屬性
    onPointerDown: (event) => print("onPointerDown")
)
複製代碼

當咱們再次點擊Text之外的區域時,能夠發現命中列表中加入了RenderPointerListener

由於當RenderPointerListener執行hitTestSelf時,判斷behavior若是爲HitTestBehavior.opaque,則返回true。也就是說RenderPointerListener符合命中測試。

因此,咱們能看到控制檯將會打印onPointerDown

再舉個例子

爲了更深刻的理解behavior屬性,咱們再來看另一個例子。

Stack(
  children: <Widget>[
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(400, 200)),
        child: Container(
          color: Colors.blue,
        )
      ),
      onPointerDown: (event) => print("onPointerDown1"),
    ),
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(400, 200)),
        child: Center(child: Text("dont click me")),
      ),
      onPointerDown: (event) => print("onPointerDown2"),
//    behavior: HitTestBehavior.opaque, //註釋1
//    behavior: HitTestBehavior.translucent,  //註釋2
    )
  ],
),
複製代碼

它的展現效果如上圖所示。
上圖爲 WidgetRenderObject的對應關係。

一、behavior爲默認HitTestBehavior.deferToChild屬性時,當點擊了Text之外的區域,它的命中測試列表是這樣的: RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack

RenderStackhitTestChildren會先找Stack中最上層的child,看它是否命中測試。很顯然,第一個child,即第二個Listener沒有命中測試。

而後它再去找第二個child,即第一個Listener是否命中測試。這裏的第一個Listener包含的Container設置了color屬性,因此Container這裏對應的是RenderDecoratedBox,它經過了命中測試,相應的Listener也經過了命中測試。

因此控制檯會只打印onPointerDown1

二、將註釋2關閉,註釋1打開,behaviorHitTestBehavior.opaque屬性時,當點擊了Text之外的區域,它的命中測試列表是這樣的: RenderPointerListener->RenderStack

RenderStackhitTestChildren會先找Stack中最上層的child,看它是否命中測試。第一個child,即第二個Listener加上了HitTestBehavior.opaque屬性後,經過了命中測試。

這個時候RenderStackhitTestChildren直接返回了true,它並不會再去檢測第二個child,即第一個Listener是否命中測試。

因此控制檯只會打印onPointerDown2

三、將註釋1關閉,註釋2打開,behaviorHitTestBehavior.translucent屬性時,當點擊了Text之外的區域,它的命中測試列表是這樣的: RenderPointerListener->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack

RenderStackhitTestChildren會先找Stack中最上層的child,看它是否命中測試。第一個child,即第二個Listener加上了HitTestBehavior.translucent屬性後,經過了命中測試,加入命中測試列表。但必須注意的是,雖然經過了命中測試,可是該RenderPointerListener的hitTest方法返回false

而後RenderStack會再去找第二個child,即第一個Listener是否命中測試。由上面的分析可知,它是經過了命中測試的。所以整個命中測試列表就是: RenderPointerListener->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack

因此控制檯會先打印onPointerDown2,而後再打印onPointerDown1

總結

FlutterListener組件是一切可觸控Widget的包裝組件,在觸摸事件肯定怎麼樣傳遞時,須要對Widget進行命中測試。Listener提供了behavior屬性,可靈活的改變Listener在命中測試時的表現,提供多種不同的觸控表現。

相關文章
相關標籤/搜索