深入理解Flutter的Listener組件

引言

有過(guò)移動(dòng)端開(kāi)發(fā)經(jīng)驗(yàn)的同學(xué)都知道刹帕,移動(dòng)端的觸摸事件是由手指按下吵血、手指移動(dòng)、手指抬起這些基本事件組成的偷溺。

Flutter中蹋辅,一切皆WidgetWidget本身并不具備識(shí)別觸摸事件的功能挫掏。能識(shí)別觸摸事件的Widget侦另,必須經(jīng)由ListenerGestureDetector組裝起來(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)最底層Widgetchildren 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")
)
image

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

image

Flutter中冬三,每一個(gè)Widget實(shí)際上會(huì)對(duì)應(yīng)一個(gè)RenderObject匀油。對(duì)于上面代碼來(lái)說(shuō),上圖為WidgetRenderObject的對(duì)應(yīng)關(guān)系勾笆。

1敌蚜、當(dāng)點(diǎn)擊了Text時(shí),它的命中測(cè)試列表是這樣的:
RenderParagraph->RenderPositionedBox->RenderConstrainedBox->RenderPointerListener窝爪,所以RenderPointerListenerhandleEvent方法會(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)致RenderConstrainedBoxhitTestChildren返回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è)試方法是RenderBoxRenderObject的子類)的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è)試堤撵,就是看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是否命中測(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è)例子孝情,我們將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")
)

當(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
    )
  ],
),

image

它的展示效果如上圖所示镀赌。
image

上圖為WidgetRenderObject的對(duì)應(yīng)關(guān)系。

1际跪、behavior為默認(rèn)HitTestBehavior.deferToChild屬性時(shí)商佛,當(dāng)點(diǎn)擊了Text以外的區(qū)域,它的命中測(cè)試列表是這樣的:
RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack姆打。

RenderStackhitTestChildren會(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)佛析,behaviorHitTestBehavior.opaque屬性時(shí)益老,當(dāng)點(diǎn)擊了Text以外的區(qū)域,它的命中測(cè)試列表是這樣的:
RenderPointerListener->RenderStack寸莫。

RenderStackhitTestChildren會(huì)先找Stack中最上層的child捺萌,看它是否命中測(cè)試。第一個(gè)child储狭,即第二個(gè)Listener加上了HitTestBehavior.opaque屬性后互婿,通過(guò)了命中測(cè)試捣郊。

這個(gè)時(shí)候RenderStackhitTestChildren直接返回了true,它并不會(huì)再去檢測(cè)第二個(gè)child慈参,即第一個(gè)Listener是否命中測(cè)試呛牲。

所以控制臺(tái)只會(huì)打印onPointerDown2

3驮配、將注釋1關(guān)閉娘扩,注釋2打開(kāi),behaviorHitTestBehavior.translucent屬性時(shí)壮锻,當(dāng)點(diǎn)擊了Text以外的區(qū)域琐旁,它的命中測(cè)試列表是這樣的:
RenderPointerListener->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack

RenderStackhitTestChildren會(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é)

FlutterListener組件是一切可觸控Widget的包裝組件廊谓,在觸摸事件確定怎么樣傳遞時(shí),需要對(duì)Widget進(jìn)行命中測(cè)試麻削。Listener提供了behavior屬性,可靈活的改變Listener在命中測(cè)試時(shí)的表現(xiàn)春弥,提供多種不一樣的觸控表現(xiàn)呛哟。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市匿沛,隨后出現(xiàn)的幾起案子扫责,更是在濱河造成了極大的恐慌,老刑警劉巖逃呼,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳖孤,死亡現(xiàn)場(chǎng)離奇詭異者娱,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)苏揣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)黄鳍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人平匈,你說(shuō)我怎么就攤上這事框沟。” “怎么了增炭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵忍燥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我隙姿,道長(zhǎng)梅垄,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任输玷,我火速辦了婚禮哎甲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饲嗽。我一直安慰自己炭玫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布貌虾。 她就那樣靜靜地躺著吞加,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尽狠。 梳的紋絲不亂的頭發(fā)上衔憨,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音袄膏,去河邊找鬼践图。 笑死,一個(gè)胖子當(dāng)著我的面吹牛沉馆,可吹牛的內(nèi)容都是我干的码党。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼斥黑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼揖盘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起锌奴,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤兽狭,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體箕慧,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡服球,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颠焦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斩熊。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蒸健,靈堂內(nèi)的尸體忽然破棺而出座享,到底是詐尸還是另有隱情,我是刑警寧澤似忧,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布渣叛,位于F島的核電站,受9級(jí)特大地震影響盯捌,放射性物質(zhì)發(fā)生泄漏淳衙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一饺著、第九天 我趴在偏房一處隱蔽的房頂上張望箫攀。 院中可真熱鬧,春花似錦幼衰、人聲如沸靴跛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)梢睛。三九已至,卻和暖如春识椰,著一層夾襖步出監(jiān)牢的瞬間绝葡,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工腹鹉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留藏畅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓功咒,卻偏偏與公主長(zhǎng)得像愉阎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子航瞭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 引言 上一篇文章 深入理解Flutter的Listener組件介紹了觸控事件的監(jiān)聽(tīng)原理诫硕,讓我們對(duì)Flutter中觸...
    AndroidHint閱讀 1,894評(píng)論 0 3
  • 在移動(dòng)端所謂的用戶交互事件既是用戶的手勢(shì)操作處理。手勢(shì)操作在flutter中可分為兩類: 第一類是原始的指針事件(...
    FluOrAnd閱讀 2,074評(píng)論 0 1
  • 每日一言:我們的手中刊侯,握著的可能是失敗的種子,也可能是成功的無(wú)限潛能锉走,答案需要我們自己選擇:隨波逐浪將一事無(wú)成滨彻,全...
    乘香墨影閱讀 1,124評(píng)論 0 9
  • 雖然預(yù)知死期是我喜歡的一種生命結(jié)束的方式藕届,可是我仍然拒絕死亡。在這世上有三個(gè)與我個(gè)人死亡牢牢相連的生命亭饵,那便是父親...
    越兒笑傾城閱讀 92評(píng)論 1 1
  • 今天我想寫(xiě)點(diǎn)關(guān)于我眼中的魔都休偶,上海。 昨天刷抖音辜羊,一連幾條推送都是關(guān)于這幾天上海外灘在試運(yùn)行燈光秀的絢爛奪目畫(huà)面踏兜,...
    周Jourway閱讀 408評(píng)論 0 1