有过移动端开发经验的同窗都知道,移动端的触摸事件是由手指按下、手指移动、手指抬起这些基本事件组成的。bash
在Flutter
中,一切皆Widget
。Widget
自己并不具有识别触摸事件的功能。能识别触摸事件的Widget
,必须经由Listener
或GestureDetector
组装起来。markdown
而GestureDetector
本质上仍是由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提供了多种触摸事件的监听,但咱们常常用到的是onPointerDown
、onPointerMove
、onPointerUp
,分别对应手指按下、手指移动、手指抬起这三个触摸事件。this
child
属性表示被包装的Widget
。spa
behavior
属性,这是Listener
很重要的一个属性,也是本节着重讨论的,可是如今还轮不到他出场,在理解behavior
属性以前,咱们必需要认识一个概念,叫作命中测试(Hit Test)。code
当手指按下、移动或者抬起时,Flutter
会给每个事件新建一个对象,如按下是PointerDownEvent
,移动是PointerMoveEvent
,抬起是PointerUpEvent
。对于每个事件对象,Flutter
都会执行命中测试,它经历了如下这几步:orm
一、从最底层的Widget
开始执行命中测试,是否命中取决于hitTestChildren
方法(它的children Widget
是否命中测试)或hitTestSelf
方法是否返回true
。对象
二、循环最底层Widget
的children Widget
,分别执行child Widget
的命中测试。child Widget
是否命中也取决于hitTestChidren
方法(它的children Widget
是否命中测试)或hitTestSelf
方法是否返回true
。继承
三、从下往上递归地执行命中测试,直到找到最上层的一个命中测试的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
。对于上面代码来讲,上图为Widget
和RenderObject
的对应关系。
一、当点击了Text
时,它的命中测试列表是这样的: RenderParagraph
->RenderPositionedBox
->RenderConstrainedBox
->RenderPointerListener
,因此RenderPointerListener
的handleEvent
方法会被执行,最终在控制台会打印onPointerDown。
注意:触摸事件会循环命中测试列表,并分别执行它们的
handleEvent
方法。Flutter
中几乎全部Widget
对应的RenderObject
都是直接或者间接继承自RenderBox
,而RenderBox
继承了HitTestTarget,并重写了handleEvent
方法。
二、当点击了Text
之外的区域时,它的命中测试列表就没有RenderPointerListener
了。为何呢???
Text
之外的区域是ConstrainedBox
的(为何不是Center
,由于Center
的功能是帮助Text
定位,它的区域和Text
是一致的)。那ConstrainedBox
对应的RenderConstrainedBox
命中测试了么?很显然是没有的。
由于ConstrainedBox
只有一个child
,就是Center
。Center
对应的RenderPositionedBox
没有命中测试,致使RenderConstrainedBox
的hitTestChildren
返回false
,而它的hitTestSelf
也返回false
,因此RenderConstrainedBox
没有命中测试。
而Listener
也只有一个child
,那就是ConstrainedBox
,既然RenderConstrainedBox
没有命中测试,那么RenderPointerListener
相应的就没有命中测试,因此命中测试列表中是没有RenderPointerListener
的。
因此控制台并不会打印onPointerDown。
说明:命中测试方法是
RenderBox
(RenderObject
的子类)的hitTest
方法。
上面的例子使用的behavior
属性是默认的HitTestBehavior.deferToChild
,若是修改一下behavior
属性会有什么奇妙的效果呢?
behavior
表示命中测试(Hit Test)过程当中的表现策略。它是一个枚举,提供了三个值,分别是HitTestBehavior.deferToChild
、HitTestBehavior.opaque
、HitTestBehavior.translucent
。
上面说到过,命中测试,就是看RenderBox
的hitTest
的返回值,如Listener
的hitTest
方法以下。
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.deferToChild:Listener
是否命中测试,取决于子child
是否命中测试,这是默认behavior
的默认值。
HitTestBehavior.opaque:当Listener
的子child
没有命中测试时,该属性值保证hitTestSelf
返回true
,即保证Listener
所在区域能响应触摸事件。
HitTestBehavior.translucent:当Listener
的子child
没有命中测试时,而且hitTestSelf
返回false
时,该属性值能够保证Listener
所在的区域能响应触摸事件(加入到命中测试列表),可是hitTest
方法返回值仍是false
,这不能改变。
上面那个例子,咱们将Listener
的behavior
属性修改成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 ) ], ), 复制代码
Widget
与
RenderObject
的对应关系。
一、behavior
为默认HitTestBehavior.deferToChild
属性时,当点击了Text
之外的区域,它的命中测试列表是这样的: RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
会先找Stack
中最上层的child
,看它是否命中测试。很显然,第一个child
,即第二个Listener
没有命中测试。
而后它再去找第二个child
,即第一个Listener
是否命中测试。这里的第一个Listener
包含的Container
设置了color
属性,因此Container
这里对应的是RenderDecoratedBox
,它经过了命中测试,相应的Listener
也经过了命中测试。
因此控制台会只打印onPointerDown1。
二、将注释2关闭,注释1打开,behavior
为HitTestBehavior.opaque
属性时,当点击了Text
之外的区域,它的命中测试列表是这样的: RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
会先找Stack
中最上层的child
,看它是否命中测试。第一个child
,即第二个Listener
加上了HitTestBehavior.opaque
属性后,经过了命中测试。
这个时候RenderStack
的hitTestChildren
直接返回了true
,它并不会再去检测第二个child
,即第一个Listener
是否命中测试。
因此控制台只会打印onPointerDown2。
三、将注释1关闭,注释2打开,behavior
为HitTestBehavior.translucent
属性时,当点击了Text
之外的区域,它的命中测试列表是这样的: RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
RenderStack
的hitTestChildren
会先找Stack
中最上层的child
,看它是否命中测试。第一个child
,即第二个Listener
加上了HitTestBehavior.translucent
属性后,经过了命中测试,加入命中测试列表。但必须注意的是,虽然经过了命中测试,可是该RenderPointerListener的hitTest方法返回false。
而后RenderStack会再去找第二个child
,即第一个Listener
是否命中测试。由上面的分析可知,它是经过了命中测试的。所以整个命中测试列表就是: RenderPointerListener
->RenderDecoratedBox
->RenderConstrainedBox
->RenderPointerListener
->RenderStack
。
因此控制台会先打印onPointerDown2,而后再打印onPointerDown1。
Flutter
的Listener
组件是一切可触控Widget
的包装组件,在触摸事件肯定怎么样传递时,须要对Widget
进行命中测试。Listener
提供了behavior
属性,可灵活的改变Listener
在命中测试时的表现,提供多种不同的触控表现。