深入理解Flutter的Listener组件

引言

有过移动端开发经验的同学都知道,移动端的触摸事件是由手指按下、手指移动、手指抬起这些基本事件组成的。

Flutter中,一切皆WidgetWidget本身并不具备识别触摸事件的功能。能识别触摸事件的Widget,必须经由ListenerGestureDetector组装起来。

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,分别对应手指按下手指移动手指抬起这三个触摸事件。

child属性表示被包装的Widget

behavior属性,这是Listener很重要的一个属性,也是本节着重讨论的,但是现在还轮不到他出场,在理解behavior属性之前,我们必须要认识一个概念,叫做命中测试(Hit Test)

一、命中测试

当手指按下、移动或者抬起时,Flutter会给每一个事件新建一个对象,如按下是PointerDownEvent,移动是PointerMoveEvent,抬起是PointerUpEvent。对于每一个事件对象,Flutter都会执行命中测试,它经历了以下这几步:

1、从最底层的Widget开始执行命中测试,是否命中取决于hitTestChildren方法(它的children Widget是否命中测试)或hitTestSelf方法是否返回true

2、循环最底层Widgetchildren Widget,分别执行child Widget的命中测试。child Widget是否命中也取决于hitTestChidren方法(它的children Widget是否命中测试)或hitTestSelf方法是否返回true

3、从下往上递归地执行命中测试,直到找到最上层的一个命中测试的Widget,将它加入命中测试列表。由于它已命中测试,那么它的父Widget也命中了测试,将父Widget也加入命中测试列表。以此类推,直到将所有命中测试的Widget加入命中测试列表。

举个例子

为了更加形象的理解命中测试这个概念,我们看一下下面的例子。

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

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

image

Flutter中,每一个Widget实际上会对应一个RenderObject。对于上面代码来说,上图为WidgetRenderObject的对应关系。

1、当点击了Text时,它的命中测试列表是这样的:
RenderParagraph->RenderPositionedBox->RenderConstrainedBox->RenderPointerListener,所以RenderPointerListenerhandleEvent方法会被执行,最终在控制台会打印onPointerDown

注意:触摸事件会循环命中测试列表,并分别执行它们的handleEvent方法。Flutter中几乎所有Widget对应的RenderObject都是直接或者间接继承自RenderBox,而RenderBox继承了HitTestTarget,并重写了handleEvent方法。

2、当点击了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属性会有什么奇妙的效果呢?

二、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
    )
  ],
),

image

它的展示效果如上图所示。
image

上图为WidgetRenderObject的对应关系。

1、behavior为默认HitTestBehavior.deferToChild属性时,当点击了Text以外的区域,它的命中测试列表是这样的:
RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack

RenderStackhitTestChildren会先找Stack中最上层的child,看它是否命中测试。很显然,第一个child,即第二个Listener没有命中测试。

然后它再去找第二个child,即第一个Listener是否命中测试。这里的第一个Listener包含的Container设置了color属性,所以Container这里对应的是RenderDecoratedBox,它通过了命中测试,相应的Listener也通过了命中测试。

所以控制台会只打印onPointerDown1

2、将注释2关闭,注释1打开,behaviorHitTestBehavior.opaque属性时,当点击了Text以外的区域,它的命中测试列表是这样的:
RenderPointerListener->RenderStack

RenderStackhitTestChildren会先找Stack中最上层的child,看它是否命中测试。第一个child,即第二个Listener加上了HitTestBehavior.opaque属性后,通过了命中测试。

这个时候RenderStackhitTestChildren直接返回了true,它并不会再去检测第二个child,即第一个Listener是否命中测试。

所以控制台只会打印onPointerDown2

3、将注释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在命中测试时的表现,提供多种不一样的触控表现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 引言 上一篇文章 深入理解Flutter的Listener组件介绍了触控事件的监听原理,让我们对Flutter中触...
    AndroidHint阅读 1,777评论 0 3
  • 在移动端所谓的用户交互事件既是用户的手势操作处理。手势操作在flutter中可分为两类: 第一类是原始的指针事件(...
    FluOrAnd阅读 2,008评论 0 1
  • 每日一言:我们的手中,握着的可能是失败的种子,也可能是成功的无限潜能,答案需要我们自己选择:随波逐浪将一事无成,全...
    乘香墨影阅读 1,089评论 0 9
  • 虽然预知死期是我喜欢的一种生命结束的方式,可是我仍然拒绝死亡。在这世上有三个与我个人死亡牢牢相连的生命,那便是父亲...
    越儿笑倾城阅读 86评论 1 1
  • 今天我想写点关于我眼中的魔都,上海。 昨天刷抖音,一连几条推送都是关于这几天上海外滩在试运行灯光秀的绚烂夺目画面,...
    周Jourway阅读 390评论 0 1