Listener

Listener

背景:

  • 可能會(huì)遇到的問題:如下代碼,點(diǎn)擊文字以外的區(qū)域是無響應(yīng)的
GestureDetector(
      child: Container(
        height: 50,
        // color: Colors.green,
        padding: EdgeInsets.only(left: 5, right: 5),
        alignment: Alignment.center,
        child: Text(
          "click me",
          style: TextStyle(fontSize: 20),
        )
      ),
      onTap: () {
        print("click");
      },
    )

原因分析:

GestureDetector -> RawGestureDetector -> Listener

Listener是一個(gè)監(jiān)聽指針事件的控件腕柜,比如按下辐真、移動(dòng)、釋放殃姓、取消等指針事件命满。

通常情況下蔬崩,監(jiān)聽手勢(shì)事件使用GestureDetector,GestureDetector是更高級(jí)的手勢(shì)事件便脊。

Listener的事件介紹如下:
onPointerDown:按下時(shí)回調(diào)
onPointerMove:移動(dòng)時(shí)回調(diào)
onPointerUp:抬起時(shí)回調(diào)

  • 用法如下:
Listener(
  onPointerDown: (PointerDownEvent pointerDownEvent) {
    print('$pointerDownEvent');
  },
  onPointerMove: (PointerMoveEvent pointerMoveEvent) {
    print('$pointerMoveEvent');
  },
  onPointerUp: (PointerUpEvent upEvent) {
    print('$upEvent');
  },
  child: Container(
    height: 200,
    width: 200,
    color: Colors.blue,
    alignment: Alignment.center,
  ),
)
  • 當(dāng)手指按下時(shí)蚂四,F(xiàn)lutter會(huì)對(duì)應(yīng)用程序執(zhí)行命中測(cè)試(Hit Test),以確定指針與屏幕接觸的位置存在哪些組件(widget)哪痰, 指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測(cè)試發(fā)現(xiàn)的最內(nèi)部的組件遂赠,然后從那里開始,事件會(huì)在組件樹中向上冒泡晌杰,這些事件會(huì)從最內(nèi)部的組件被分發(fā)到組件樹根的路徑上的所有組件跷睦,這和Web開發(fā)中瀏覽器的事件冒泡機(jī)制相似, 但是Flutter中沒有機(jī)制取消或停止“冒泡”過程肋演,而瀏覽器的冒泡是可以停止的抑诸。注意烂琴,只有通過命中測(cè)試的組件才能觸發(fā)事件。

什么是命中測(cè)試哼鬓?

當(dāng)手指按下监右、移動(dòng)或者抬起時(shí),F(xiàn)lutter會(huì)給每一個(gè)事件新建一個(gè)對(duì)象异希,如按下是PointerDownEvent健盒,移動(dòng)是PointerMoveEvent,抬起是PointerUpEvent称簿。對(duì)于每一個(gè)事件對(duì)象扣癣,F(xiàn)lutter都會(huì)執(zhí)行命中測(cè)試,它經(jīng)歷了以下這幾步:

1憨降、從最底層的Widget開始執(zhí)行命中測(cè)試父虑,是否命中取決于hitTestChildren方法(它的children Widget是否命中測(cè)試)或hitTestSelf方法是否返回true, 如果返回true授药,表示命中測(cè)試通過士嚎,會(huì)把自己以HitTestEntry添加到HitTestResult對(duì)象中。
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è)試列表猫十。

原則:優(yōu)先判斷children,再判斷自己览濒,只要有一個(gè)為true,就把自己加入到result中


bool hitTest(BoxHitTestResult result, { @required Offset position }) {
    // 事件的position必須在當(dāng)前組件內(nèi)
  if (_size.contains(position)) {
      // 優(yōu)先判斷children,再判斷自己,只要有一個(gè)為true,就把自己加入到result中
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

測(cè)試列表鏈:

  • 在Flutter中炫彩,每一個(gè)Widget實(shí)際上會(huì)對(duì)應(yīng)一個(gè)RenderObject。對(duì)于上面代碼來說絮短,上圖為Widget和RenderObject的對(duì)應(yīng)關(guān)系江兢。

例:

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

1、當(dāng)點(diǎn)擊了Text時(shí)丁频,它的命中測(cè)試列表是這樣的:
RenderParagraph->RenderPositionedBox->RenderConstrainedBox->RenderPointerListener杉允,
所以RenderPointerListener的handleEvent方法會(huì)被執(zhí)行邑贴,最終在控制臺(tái)會(huì)打印onPointerDown。

2叔磷、當(dāng)點(diǎn)擊了Text以外的區(qū)域時(shí)拢驾,它的命中測(cè)試列表就沒有RenderPointerListener了。為什么呢改基?繁疤??
Text以外的區(qū)域是ConstrainedBox的(為什么不是Center秕狰,因?yàn)镃enter的功能是幫助Text定位稠腊,它的區(qū)域和Text是一致的)。那ConstrainedBox對(duì)應(yīng)的RenderConstrainedBox命中測(cè)試了么鸣哀?很顯然是沒有的架忌。
因?yàn)镃onstrainedBox只有一個(gè)child,就是Center我衬。Center對(duì)應(yīng)的RenderPositionedBox沒有命中測(cè)試叹放,導(dǎo)致RenderConstrainedBox的hitTestChildren返回false,而它的hitTestSelf也返回false挠羔,所以RenderConstrainedBox沒有命中測(cè)試井仰。
而Listener也只有一個(gè)child,那就是ConstrainedBox褥赊,既然RenderConstrainedBox沒有命中測(cè)試糕档,那么RenderPointerListener相應(yīng)的就沒有命中測(cè)試,所以命中測(cè)試列表中是沒有RenderPointerListener的拌喉。
所以控制臺(tái)并不會(huì)打印onPointerDown速那。

上面的例子使用的behavior屬性是默認(rèn)的HitTestBehavior.deferToChild,如果修改一下behavior屬性會(huì)有什么奇妙的效果呢尿背?

一端仰、behavior:

behavior表示命中測(cè)試(Hit Test)過程中的表現(xiàn)策略。它是一個(gè)枚舉田藐,提供了三個(gè)值荔烧,
分別是HitTestBehavior.deferToChild、HitTestBehavior.opaque汽久、HitTestBehavior.translucent

/// How to behave during hit tests.
enum HitTestBehavior {
  ///事件是否處理取決于自己的子類
  deferToChild,

  /// Opaque targets can be hit by hit tests, causing them to both receive
  /// events within their bounds and prevent targets visually behind them from
  /// also receiving events.
  /// 自己可以命中hitTest鹤竭,又在視覺上阻止位于其后方的目標(biāo)也接收事件。
  opaque,

  /// Translucent targets both receive events within their bounds and permit
  /// targets visually behind them to also receive events.
  ///半透明目標(biāo)既可以接收其范圍內(nèi)的事件景醇,也可以在視覺上允許目標(biāo)后面的目標(biāo)也接收事件臀稚。
  translucent,
}

  • 源碼引用順序 Listener->_PointerListener->RenderPointerListener->RenderProxyBoxWithHitTestBehavior

源碼分析:

RenderProxyBoxWithHitTestBehavior源碼,代碼很少三痰,但邏輯就是在這里了
/// A RenderProxyBox subclass that allows you to customize the
/// hit-testing behavior.
abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {

  @override
  bool hitTest(BoxHitTestResult result, { Offset position }) {
    bool hitTarget = false;
    // position在自己范圍內(nèi)
    if (size.contains(position)) {
      // 判斷子類和自己是否命中吧寺,這是普通邏輯窜管,沒什么特別的
      hitTarget =
      hitTestChildren(result, position: position) || hitTestSelf(position);
      // 如果是HitTestBehavior.translucent,強(qiáng)行將自己命中hittest稚机,參與事件消費(fèi)的隊(duì)列中幕帆,這里hitTestChildren和hitTestSelf的結(jié)果就不重要了
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

 //如果Behavior是opaque,且沒有被子類重寫赖条,那就是返回true失乾,也即是參與到事件消費(fèi)的隊(duì)列中
  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
}

所以,當(dāng)我們用GestureDetector監(jiān)聽事件時(shí)谋币,最后都會(huì)走到RenderProxyBoxWithHitTestBehavior里仗扬,只要behavio是opaque或translucent都會(huì)將自己加入到事件消費(fèi)的隊(duì)列, 而behavior默認(rèn)是HitTestBehavior.deferToChild蕾额,當(dāng)點(diǎn)擊空白處時(shí)早芭,
hitTestChildren(result, position: position)返回false
hitTestSelf(hitTestSelf) 也返回false

二、背景色:

接下來看第二個(gè)問題诅蝶,為什么Container設(shè)置任意背景色也可以響應(yīng)點(diǎn)擊事件退个?

Container其實(shí)是個(gè)StateLessWidget,它本身并沒有RenderObject對(duì)應(yīng)调炬,可以理解為是個(gè)配置項(xiàng)语盈,真實(shí)渲染的render是其他配置引進(jìn)的,比如color對(duì)應(yīng)的ColoredBox
ColoredBox->_RenderColoredBox

// 一眼就看到了缰泡,強(qiáng)制設(shè)置為opaque了刀荒,答案和第一個(gè)問題一樣了
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
  _RenderColoredBox({@required Color color})
    : _color = color,
     //看這里!!!!!!!!!!!!!!!!!!!!!
      super(behavior: HitTestBehavior.opaque);
    
}

所以,Container設(shè)置任意背景色棘钞,可以響應(yīng)點(diǎn)擊事件缠借,因?yàn)樵O(shè)置了color后,返回的widget里包含了ColoredBox宜猜,ColoredBox對(duì)應(yīng)的RenderObject是_RenderColoredBox泼返,_RenderColoredBox繼承自RenderProxyBoxWithHitTestBehavior并強(qiáng)制指定了behavior是HitTestBehavior.opaque。

下面例子進(jìn)一步印證:

return Stack(
      children: <Widget>[
        Listener(
          child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(300.0, 300.0)),
            child: DecoratedBox(decoration: BoxDecoration(color: Colors.red)),
          ),
          onPointerDown: (event) => print("first child"),
        ),
        Listener(
          child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(200.0, 200.0)),
            child: Center(child: Text("左上角200*200范圍內(nèi)-空白區(qū)域點(diǎn)擊")),
          ),
          onPointerDown: (event) => print("second child"),
          //放開此行注釋后姨拥,單詞點(diǎn)擊 first ,second都會(huì)響應(yīng)绅喉,HitTestBehavior.opaque是不行的
          // behavior: HitTestBehavior.translucent,
        )
      ],
    );

當(dāng)點(diǎn)擊左上角,非文本區(qū)域時(shí)叫乌,只會(huì)響應(yīng)first child柴罐,當(dāng)把這句代碼注釋打開

// behavior: HitTestBehavior.translucent,

會(huì)先輸出second child,再輸出first child憨奸。原因還是在RenderProxyBoxWithHitTestBehavior的hitTest方法

@override
  bool hitTest(BoxHitTestResult result, { Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      //如果是translucent革屠,盡快自身加入了命中測(cè)試隊(duì)列,但返回的結(jié)果還是false,
      //但如果是opaque屠阻,子類不重寫hitTestSelf,那hitTarget肯定就是true了
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

對(duì)于Stack來說额各,對(duì)應(yīng)的Render是RenderStack

RenderStack hitTestChildren
 @override
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
    return defaultHitTestChildren(result, position: position);
  }


最終走到了RenderBoxContainerDefaultsMixin的defaultHitTestChildren

  bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
    // The x, y parameters have the top left of the node's box as the origin.
    // 從最上層的子類開始遍歷
    ChildType child = lastChild;
    while (child != null) {
      final ParentDataType childParentData = child.parentData as ParentDataType;
      final bool isHit = result.addWithPaintOffset(
        offset: childParentData.offset,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(transformed == position - childParentData.offset);
          return child.hitTest(result, position: transformed);
        },
      );
      // 第一個(gè)命中国觉,就直接返回了,后續(xù)的子類不再執(zhí)行命中測(cè)試虾啦,所以translucent能透?jìng)髀榫鳎驗(yàn)楸凰揎椀腖istener,返回的結(jié)果是false
      if (isHit)
        return true;
      child = childParentData.previousSibling;
    }
    return false;
  }

結(jié)論:

所以想要解決開頭拋出的問題,方法如下:
1傲醉、GestureDetector的behavior設(shè)置為opaque或者translucent才行蝇闭,
2、Container設(shè)置任意背景色

總結(jié):

opaque和translucent的區(qū)別:
RenderStack的hitTestChildren返回了true硬毕,它就不會(huì)再去檢測(cè)第二個(gè)child呻引。
opaque: 第一個(gè)Listener是否命中測(cè)試” ,即意味著如果第一個(gè)child的hitTest返回true(例如opaque)的話Stack就不會(huì)再把指針事件傳給第二個(gè)child吐咳,即不能透?jìng)?/strong>逻悠,
translucent: 如果第一個(gè)child的hitTest返回false(例如translucent)則點(diǎn)擊事件會(huì)被傳遞到第二個(gè)child,即能透?jìng)?/strong>

截屏2021-08-25 下午1.56.36.png

Stack

參考:
https://juejin.cn/post/6844904079106277383
https://juejin.cn/post/6908365134365491208

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末韭脊,一起剝皮案震驚了整個(gè)濱河市童谒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沪羔,老刑警劉巖饥伊,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蔫饰,居然都是意外死亡琅豆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門死嗦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來趋距,“玉大人,你說我怎么就攤上這事越除〗诟” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵摘盆,是天一觀的道長(zhǎng)翼雀。 經(jīng)常有香客問我,道長(zhǎng)孩擂,這世上最難降的妖魔是什么狼渊? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上狈邑,老公的妹妹穿的比我還像新娘城须。我一直安慰自己,他們只是感情好米苹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布糕伐。 她就那樣靜靜地躺著,像睡著了一般蘸嘶。 火紅的嫁衣襯著肌膚如雪良瞧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天训唱,我揣著相機(jī)與錄音褥蚯,去河邊找鬼。 笑死况增,一個(gè)胖子當(dāng)著我的面吹牛赞庶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澳骤,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼尘执,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了宴凉?” 一聲冷哼從身側(cè)響起誊锭,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弥锄,沒想到半個(gè)月后丧靡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡籽暇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年温治,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戒悠。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡熬荆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绸狐,到底是詐尸還是另有隱情卤恳,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布寒矿,位于F島的核電站突琳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏符相。R本人自食惡果不足惜拆融,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镜豹,春花似錦傲须、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至散怖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肄渗,已是汗流浹背镇眷。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翎嫡,地道東北人欠动。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像惑申,于是被迫代替她去往敵國(guó)和親具伍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • 在移動(dòng)端所謂的用戶交互事件既是用戶的手勢(shì)操作處理圈驼。手勢(shì)操作在flutter中可分為兩類: 第一類是原始的指針事件(...
    FluOrAnd閱讀 2,062評(píng)論 0 1
  • 引言 有過移動(dòng)端開發(fā)經(jīng)驗(yàn)的同學(xué)都知道人芽,移動(dòng)端的觸摸事件是由手指按下、手指移動(dòng)绩脆、手指抬起這些基本事件組成的萤厅。 在Fl...
    AndroidHint閱讀 2,480評(píng)論 0 6
  • 1、const 變量是一個(gè)編譯時(shí)常量靴迫,final變量在第一次使用時(shí)被初始化惕味。被final或者const修飾的變量,...
    buhuiming閱讀 378評(píng)論 0 0
  • 什么是Widget? 一切皆是Widget玉锌,Widget是Flutter應(yīng)用用戶界面的基本單元名挥,每個(gè)Widget都...
    佼佼者M(jìn)r閱讀 392評(píng)論 0 0
  • 為了應(yīng)對(duì)復(fù)雜的業(yè)務(wù)場(chǎng)景,同時(shí)降低侵入性主守,在保持api穩(wěn)定基礎(chǔ)上禀倔,全面重構(gòu)了SmartDialog底層我現(xiàn)在可以自信...
    小呆呆666閱讀 3,059評(píng)論 2 9