美文网首页
深入理解Flutter的Listener组件

深入理解Flutter的Listener组件

作者: AndroidHint | 来源:发表于2020-03-15 11:44 被阅读0次

    引言

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

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

    相关文章

      网友评论

          本文标题:深入理解Flutter的Listener组件

          本文链接:https://www.haomeiwen.com/subject/ywrcehtx.html