引言
有過(guò)移動(dòng)端開(kāi)發(fā)經(jīng)驗(yàn)的同學(xué)都知道刹帕,移動(dòng)端的觸摸事件是由手指按下吵血、手指移動(dòng)、手指抬起這些基本事件組成的偷溺。
在Flutter
中蹋辅,一切皆Widget
。Widget
本身并不具備識(shí)別觸摸事件的功能挫掏。能識(shí)別觸摸事件的Widget
侦另,必須經(jīng)由Listener
或GestureDetector
組裝起來(lái)。
而GestureDetector
本質(zhì)上還是由Listener
組成的,所以我們先認(rèn)識(shí)一下Listener
。
Listener
Listener
在功能劃分上屬于功能型Widget
捷绒,主要提供原始觸摸事件的監(jiān)聽(tīng)。下面看一下它的構(gòu)造函數(shù):
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,
})
從構(gòu)造函數(shù)中可以知道殿托,Listener提供了多種觸摸事件的監(jiān)聽(tīng),但我們經(jīng)常用到的是onPointerDown
杠河、onPointerMove
碌尔、onPointerUp
浇辜,分別對(duì)應(yīng)手指按下券敌、手指移動(dòng)、手指抬起這三個(gè)觸摸事件柳洋。
child
屬性表示被包裝的Widget
待诅。
behavior
屬性,這是Listener
很重要的一個(gè)屬性熊镣,也是本節(jié)著重討論的卑雁,但是現(xiàn)在還輪不到他出場(chǎng),在理解behavior
屬性之前绪囱,我們必須要認(rèn)識(shí)一個(gè)概念测蹲,叫做命中測(cè)試(Hit Test)。
一鬼吵、命中測(cè)試
當(dāng)手指按下扣甲、移動(dòng)或者抬起時(shí),Flutter
會(huì)給每一個(gè)事件新建一個(gè)對(duì)象齿椅,如按下是PointerDownEvent
琉挖,移動(dòng)是PointerMoveEvent
,抬起是PointerUpEvent
涣脚。對(duì)于每一個(gè)事件對(duì)象示辈,Flutter
都會(huì)執(zhí)行命中測(cè)試,它經(jīng)歷了以下這幾步:
1遣蚀、從最底層的Widget
開(kāi)始執(zhí)行命中測(cè)試矾麻,是否命中取決于hitTestChildren
方法(它的children Widget
是否命中測(cè)試)或hitTestSelf
方法是否返回true
纱耻。
2、循環(huán)最底層Widget
的children Widget
险耀,分別執(zhí)行child Widget
的命中測(cè)試膝迎。child Widget
是否命中也取決于hitTestChidren
方法(它的children Widget
是否命中測(cè)試)或hitTestSelf
方法是否返回true
。
3胰耗、從下往上遞歸地執(zhí)行命中測(cè)試限次,直到找到最上層的一個(gè)命中測(cè)試的Widget
,將它加入命中測(cè)試列表柴灯。由于它已命中測(cè)試卖漫,那么它的父Widget
也命中了測(cè)試,將父Widget
也加入命中測(cè)試列表赠群。以此類推羊始,直到將所有命中測(cè)試的Widget
加入命中測(cè)試列表。
舉個(gè)例子
為了更加形象的理解命中測(cè)試這個(gè)概念查描,我們看一下下面的例子突委。
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(200, 200)),
child: Center(
child: Text('click me'),
)
),
onPointerDown: (event) => print("onPointerDown")
)
它的展示效果如上圖所示。
在Flutter
中冬三,每一個(gè)Widget
實(shí)際上會(huì)對(duì)應(yīng)一個(gè)RenderObject
匀油。對(duì)于上面代碼來(lái)說(shuō),上圖為Widget
和RenderObject
的對(duì)應(yīng)關(guān)系勾笆。
1敌蚜、當(dāng)點(diǎn)擊了Text
時(shí),它的命中測(cè)試列表是這樣的:
RenderParagraph
->RenderPositionedBox
->RenderConstrainedBox
->RenderPointerListener
窝爪,所以RenderPointerListener
的handleEvent
方法會(huì)被執(zhí)行弛车,最終在控制臺(tái)會(huì)打印onPointerDown。
注意:觸摸事件會(huì)循環(huán)命中測(cè)試列表蒲每,并分別執(zhí)行它們的
handleEvent
方法纷跛。Flutter
中幾乎所有Widget
對(duì)應(yīng)的RenderObject
都是直接或者間接繼承自RenderBox
,而RenderBox
繼承了HitTestTarget邀杏,并重寫(xiě)了handleEvent
方法贫奠。
2、當(dāng)點(diǎn)擊了Text
以外的區(qū)域時(shí)淮阐,它的命中測(cè)試列表就沒(méi)有RenderPointerListener
了叮阅。為什么呢?泣特?浩姥?
Text
以外的區(qū)域是ConstrainedBox
的(為什么不是Center
,因?yàn)?code>Center的功能是幫助Text
定位状您,它的區(qū)域和Text
是一致的)勒叠。那ConstrainedBox
對(duì)應(yīng)的RenderConstrainedBox
命中測(cè)試了么兜挨?很顯然是沒(méi)有的。
因?yàn)?code>ConstrainedBox只有一個(gè)child
眯分,就是Center
拌汇。Center
對(duì)應(yīng)的RenderPositionedBox
沒(méi)有命中測(cè)試,導(dǎo)致RenderConstrainedBox
的hitTestChildren
返回false
弊决,而它的hitTestSelf
也返回false
噪舀,所以RenderConstrainedBox
沒(méi)有命中測(cè)試。
而Listener
也只有一個(gè)child
飘诗,那就是ConstrainedBox
与倡,既然RenderConstrainedBox
沒(méi)有命中測(cè)試,那么RenderPointerListener
相應(yīng)的就沒(méi)有命中測(cè)試昆稿,所以命中測(cè)試列表中是沒(méi)有RenderPointerListener
的纺座。
所以控制臺(tái)并不會(huì)打印onPointerDown。
說(shuō)明:命中測(cè)試方法是
RenderBox
(RenderObject
的子類)的hitTest
方法溉潭。
上面的例子使用的behavior
屬性是默認(rèn)的HitTestBehavior.deferToChild
净响,如果修改一下behavior
屬性會(huì)有什么奇妙的效果呢?
二喳瓣、behavior屬性
behavior
表示命中測(cè)試(Hit Test)過(guò)程中的表現(xiàn)策略馋贤。它是一個(gè)枚舉,提供了三個(gè)值夫椭,分別是HitTestBehavior.deferToChild
掸掸、HitTestBehavior.opaque
氯庆、HitTestBehavior.translucent
蹭秋。
上面說(shuō)到過(guò),命中測(cè)試堤撵,就是看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
是否命中測(cè)試实昨,取決于子child
是否命中測(cè)試洞豁,這是默認(rèn)behavior
的默認(rèn)值。
HitTestBehavior.opaque:當(dāng)Listener
的子child
沒(méi)有命中測(cè)試時(shí)荒给,該屬性值保證hitTestSelf
返回true
丈挟,即保證Listener
所在區(qū)域能響應(yīng)觸摸事件。
HitTestBehavior.translucent:當(dāng)Listener
的子child
沒(méi)有命中測(cè)試時(shí)志电,并且hitTestSelf
返回false
時(shí)曙咽,該屬性值可以保證Listener
所在的區(qū)域能響應(yīng)觸摸事件(加入到命中測(cè)試列表),但是hitTest
方法返回值還是false
挑辆,這不能改變例朱。
舉個(gè)例子
上面那個(gè)例子孝情,我們將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")
)
當(dāng)我們?cè)俅吸c(diǎn)擊Text
以外的區(qū)域時(shí)洒嗤,可以發(fā)現(xiàn)命中列表中加入了RenderPointerListener
箫荡。
因?yàn)楫?dāng)RenderPointerListener
執(zhí)行hitTestSelf
時(shí),判斷behavior
如果為HitTestBehavior.opaque
渔隶,則返回true
羔挡。也就是說(shuō)RenderPointerListener
符合命中測(cè)試。
所以间唉,我們能看到控制臺(tái)將會(huì)打印onPointerDown婉弹。
再舉個(gè)例子
為了更深入的理解behavior
屬性,我們?cè)賮?lái)看另外一個(gè)例子终吼。
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
的對(duì)應(yīng)關(guān)系。
1际跪、behavior
為默認(rèn)HitTestBehavior.deferToChild
屬性時(shí)商佛,當(dāng)點(diǎn)擊了Text
以外的區(qū)域,它的命中測(cè)試列表是這樣的:
RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
姆打。
RenderStack
的hitTestChildren
會(huì)先找Stack
中最上層的child
良姆,看它是否命中測(cè)試。很顯然幔戏,第一個(gè)child
玛追,即第二個(gè)Listener
沒(méi)有命中測(cè)試。
然后它再去找第二個(gè)child
闲延,即第一個(gè)Listener
是否命中測(cè)試痊剖。這里的第一個(gè)Listener
包含的Container
設(shè)置了color
屬性,所以Container
這里對(duì)應(yīng)的是RenderDecoratedBox
垒玲,它通過(guò)了命中測(cè)試陆馁,相應(yīng)的Listener
也通過(guò)了命中測(cè)試。
所以控制臺(tái)會(huì)只打印onPointerDown1合愈。
2叮贩、將注釋2關(guān)閉,注釋1打開(kāi)佛析,behavior
為HitTestBehavior.opaque
屬性時(shí)益老,當(dāng)點(diǎn)擊了Text
以外的區(qū)域,它的命中測(cè)試列表是這樣的:
RenderPointerListener
->RenderStack
寸莫。
RenderStack
的hitTestChildren
會(huì)先找Stack
中最上層的child
捺萌,看它是否命中測(cè)試。第一個(gè)child
储狭,即第二個(gè)Listener
加上了HitTestBehavior.opaque
屬性后互婿,通過(guò)了命中測(cè)試捣郊。
這個(gè)時(shí)候RenderStack
的hitTestChildren
直接返回了true
,它并不會(huì)再去檢測(cè)第二個(gè)child
慈参,即第一個(gè)Listener
是否命中測(cè)試呛牲。
所以控制臺(tái)只會(huì)打印onPointerDown2。
3驮配、將注釋1關(guān)閉娘扩,注釋2打開(kāi),behavior
為HitTestBehavior.translucent
屬性時(shí)壮锻,當(dāng)點(diǎn)擊了Text
以外的區(qū)域琐旁,它的命中測(cè)試列表是這樣的:
RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
會(huì)先找Stack
中最上層的child
猜绣,看它是否命中測(cè)試灰殴。第一個(gè)child
,即第二個(gè)Listener
加上了HitTestBehavior.translucent
屬性后掰邢,通過(guò)了命中測(cè)試牺陶,加入命中測(cè)試列表。但必須注意的是辣之,雖然通過(guò)了命中測(cè)試掰伸,但是該RenderPointerListener的hitTest方法返回false。
然后RenderStack會(huì)再去找第二個(gè)child
怀估,即第一個(gè)Listener
是否命中測(cè)試狮鸭。由上面的分析可知,它是通過(guò)了命中測(cè)試的多搀。因此整個(gè)命中測(cè)試列表就是:
RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
歧蕉。
所以控制臺(tái)會(huì)先打印onPointerDown2,然后再打印onPointerDown1酗昼。
總結(jié)
Flutter
的Listener
組件是一切可觸控Widget
的包裝組件廊谓,在觸摸事件確定怎么樣傳遞時(shí),需要對(duì)Widget
進(jìn)行命中測(cè)試麻削。Listener
提供了behavior
屬性,可靈活的改變Listener
在命中測(cè)試時(shí)的表現(xiàn)春弥,提供多種不一樣的觸控表現(xiàn)呛哟。