有過移動端開發經驗的同窗都知道,移動端的觸摸事件是由手指按下、手指移動、手指擡起這些基本事件組成的。bash
在Flutter
中,一切皆Widget
。Widget
自己並不具有識別觸摸事件的功能。能識別觸摸事件的Widget
,必須經由Listener
或GestureDetector
組裝起來。markdown
而GestureDetector
本質上仍是由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提供了多種觸摸事件的監聽,但咱們常常用到的是onPointerDown
、onPointerMove
、onPointerUp
,分別對應手指按下、手指移動、手指擡起這三個觸摸事件。this
child
屬性表示被包裝的Widget
。spa
behavior
屬性,這是Listener
很重要的一個屬性,也是本節着重討論的,可是如今還輪不到他出場,在理解behavior
屬性以前,咱們必需要認識一個概念,叫作命中測試(Hit Test)。code
當手指按下、移動或者擡起時,Flutter
會給每個事件新建一個對象,如按下是PointerDownEvent
,移動是PointerMoveEvent
,擡起是PointerUpEvent
。對於每個事件對象,Flutter
都會執行命中測試,它經歷瞭如下這幾步:orm
一、從最底層的Widget
開始執行命中測試,是否命中取決於hitTestChildren
方法(它的children Widget
是否命中測試)或hitTestSelf
方法是否返回true
。對象
二、循環最底層Widget
的children 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
。對於上面代碼來講,上圖爲Widget
和RenderObject
的對應關係。
一、當點擊了Text
時,它的命中測試列表是這樣的: RenderParagraph
->RenderPositionedBox
->RenderConstrainedBox
->RenderPointerListener
,因此RenderPointerListener
的handleEvent
方法會被執行,最終在控制檯會打印onPointerDown。
注意:觸摸事件會循環命中測試列表,並分別執行它們的
handleEvent
方法。Flutter
中幾乎全部Widget
對應的RenderObject
都是直接或者間接繼承自RenderBox
,而RenderBox
繼承了HitTestTarget,並重寫了handleEvent
方法。
二、當點擊了Text
之外的區域時,它的命中測試列表就沒有RenderPointerListener
了。爲何呢???
Text
之外的區域是ConstrainedBox
的(爲何不是Center
,由於Center
的功能是幫助Text
定位,它的區域和Text
是一致的)。那ConstrainedBox
對應的RenderConstrainedBox
命中測試了麼?很顯然是沒有的。
由於ConstrainedBox
只有一個child
,就是Center
。Center
對應的RenderPositionedBox
沒有命中測試,致使RenderConstrainedBox
的hitTestChildren
返回false
,而它的hitTestSelf
也返回false
,因此RenderConstrainedBox
沒有命中測試。
而Listener
也只有一個child
,那就是ConstrainedBox
,既然RenderConstrainedBox
沒有命中測試,那麼RenderPointerListener
相應的就沒有命中測試,因此命中測試列表中是沒有RenderPointerListener
的。
因此控制檯並不會打印onPointerDown。
說明:命中測試方法是
RenderBox
(RenderObject
的子類)的hitTest
方法。
上面的例子使用的behavior
屬性是默認的HitTestBehavior.deferToChild
,若是修改一下behavior
屬性會有什麼奇妙的效果呢?
behavior
表示命中測試(Hit Test)過程當中的表現策略。它是一個枚舉,提供了三個值,分別是HitTestBehavior.deferToChild
、HitTestBehavior.opaque
、HitTestBehavior.translucent
。
上面說到過,命中測試,就是看RenderBox
的hitTest
的返回值,如Listener
的hitTest
方法以下。
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.deferToChild:Listener
是否命中測試,取決於子child
是否命中測試,這是默認behavior
的默認值。
HitTestBehavior.opaque:當Listener
的子child
沒有命中測試時,該屬性值保證hitTestSelf
返回true
,即保證Listener
所在區域能響應觸摸事件。
HitTestBehavior.translucent:當Listener
的子child
沒有命中測試時,而且hitTestSelf
返回false
時,該屬性值能夠保證Listener
所在的區域能響應觸摸事件(加入到命中測試列表),可是hitTest
方法返回值仍是false
,這不能改變。
上面那個例子,咱們將Listener
的behavior
屬性修改成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 ) ], ), 複製代碼它的展現效果如上圖所示。 上圖爲
Widget
與
RenderObject
的對應關係。
一、behavior
爲默認HitTestBehavior.deferToChild
屬性時,當點擊了Text
之外的區域,它的命中測試列表是這樣的: RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
會先找Stack
中最上層的child
,看它是否命中測試。很顯然,第一個child
,即第二個Listener
沒有命中測試。
而後它再去找第二個child
,即第一個Listener
是否命中測試。這裏的第一個Listener
包含的Container
設置了color
屬性,因此Container
這裏對應的是RenderDecoratedBox
,它經過了命中測試,相應的Listener
也經過了命中測試。
因此控制檯會只打印onPointerDown1。
二、將註釋2關閉,註釋1打開,behavior
爲HitTestBehavior.opaque
屬性時,當點擊了Text
之外的區域,它的命中測試列表是這樣的: RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
會先找Stack
中最上層的child
,看它是否命中測試。第一個child
,即第二個Listener
加上了HitTestBehavior.opaque
屬性後,經過了命中測試。
這個時候RenderStack
的hitTestChildren
直接返回了true
,它並不會再去檢測第二個child
,即第一個Listener
是否命中測試。
因此控制檯只會打印onPointerDown2。
三、將註釋1關閉,註釋2打開,behavior
爲HitTestBehavior.translucent
屬性時,當點擊了Text
之外的區域,它的命中測試列表是這樣的: RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
會先找Stack
中最上層的child
,看它是否命中測試。第一個child
,即第二個Listener
加上了HitTestBehavior.translucent
屬性後,經過了命中測試,加入命中測試列表。但必須注意的是,雖然經過了命中測試,可是該RenderPointerListener的hitTest方法返回false。
而後RenderStack會再去找第二個child
,即第一個Listener
是否命中測試。由上面的分析可知,它是經過了命中測試的。所以整個命中測試列表就是: RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
因此控制檯會先打印onPointerDown2,而後再打印onPointerDown1。
Flutter
的Listener
組件是一切可觸控Widget
的包裝組件,在觸摸事件肯定怎麼樣傳遞時,須要對Widget
進行命中測試。Listener
提供了behavior
屬性,可靈活的改變Listener
在命中測試時的表現,提供多種不同的觸控表現。