深入理解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")
)
它的展示效果如上图所示。

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

说明:命中测试方法是RenderBox(RenderObject的子类)的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
    )
  ],
),
它的展示效果如上图所示。
上图为 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在命中测试时的表现,提供多种不一样的触控表现。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值