Flutter中的事件
事件的传递和处理
同Android一样, Flutter主要处理的事件也是DOWN、MOVE、UP、CANCEL等事件组成的事件流.
确定控件
binding.dart中的GestureBinding是Flutter事件处理链的源头, 我们就以它为线索来学习Flutter的事件处理.
1 | void _handlePointerEvent(PointerEvent event) { |
上面是GestureBinding处理事件流的函数, 也可以说是事件传递的一个预处理. 通过这个函数, Flutter可以通过事件流的位置来确定哪些控件收到了影响. Flutter将受到影响的控件按照Widget树的层次顺序排序, 然后再对它们进行事件分发.
要理解这个函数的实现, 我们需要先知道几个函数中用到的类.
1 | abstract class PointerEvent with Diagnosticable { |
首先是PointerEvent类. 显然, 函数中用到的PointerDownEvent、PointerUpEvent、PointerCancelEvent这是事件类都是这个抽象类的实现. 上面两个属性是函数中用到的两个属性, pointer是一个事件流的唯一标识. 注意, 是事件流的唯一标识. 也就是说, 同一个事件流里, DOWN、UP、CANCEL、MOVE的pointer都是相同的. 这很好理解, 但出现多点触碰的情况时, 会同时产生好几个事件流, 这时就需要通过pointer来对它们分别进行管理.
position用来标识事件发生的位置. 除了position, PointerEvent类还有一个localPosition属性. 因为Flutter是一个跨平台框架, 它在逻辑上处理事件的发生位置时应该是与设备无关的. 也就是说, Flutter框架一开始接收的position数据只是一个逻辑上的值, Flutter通过这个值来确定事件能影响到哪些控件, 但是在反映到设备上时, 使用的是localPosition的数值. 其实它们就是一个映射关系.
1 | abstract class HitTestTarget { |
然后是HitTestResult类和它所用到的两个功能类. 上面的源码是HitTestResult类源码中的节选. 通过名字可以大致猜到, 一个HitTestResult保存了一个事件流所影响的所有控件. 控件以HitTestEntry的形式存储在_path中. 对HitTestEntry再拆分, 得到的是一个HitTestTarget接口. 我们看到这个接口是有一个分发事件的handleEvent方法的, 据此我们猜测, 应该所有的控件都实现了这个接口, 通过这个接口可以来检测哪些控件受到了事件流的影响.
看到这儿我们发现, 整个函数中并没有真正把事件传递链放进_path里. 我们注意到, GestureBinding是一个mixin, 那么一定有其它类来重写了它的一些方法.
通过重写查找, 我们找到了一个继承了GestureBinding类的类: RendererBinding.
1 | abstract RendererBinding{ |
可惜的是, RendererBinding是一个抽象类, 我们还要进一步去看它拥有的RenderView.
1 | class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>{ |
RenderView继承自RenderObject, 而RenderObject又实现了HitTestTarget, 所以其实RenderView是间接实现了HitTestTarget接口的. 我们看到RenderView也有一个并非重写的的hitTest方法, 大致逻辑是先对自己的child进行检测, 然后再将自己加入到受影响的控件集合中.
RenderView的child是一个RenderBox类型的实例, RenderBox也继承自RenderObject, 所以它也间接实现了HitTestTarget接口.
1 | abstract RenderBox extends RenderObject{ |
RenderBox是一个抽象类, 所有的Widget都继承自它, 自然所有的Widget都继承了HitTestTarget接口, 这也可以证明我们之前的猜测是正确的. 从RenderBox自己拥有的hitTest方法来看, 所有Widget受某一个事件流的影响有两种情况: 它的child正在监听该事件或者它自己正在监听该事件.
简单总结一下这部分的结论: Flutter的事件流从DOWN事件开始, 经过若干个MOVE事件, 以UP事件或者CANCEL事件结束. 在处理DOWN事件时, 所有Widget按照自己继承的RenderBox类的hitTest方法依次判断自己是否受事件影响, 最终形成了以目标节点–>父节点–>根节点(也就是RenderView)–>GestureBinding的顺序构成的事件处理链, UP、MOVE、CANCEL事件都在复用DOWN事件所产生的事件处理链.
事件分发
1 | · |
GestureDetector的dispatchEvent函数实现了事件的分发. 在_handlePointerEvent函数中, 我们得到了每一个事件的处理链, dispatchEvent函数其实就是按照处理链的先后顺序来让Widget依次对事件进行响应. 这一点与Android有所不同, Android的事件传递机制中, 如果子控件处理了事件, 那么父控件是直接返回的; 在Flutter中, 子控件是否处理事件, 都不影响父控件是否处理事件.
GestureDetector的实现
这部分的内容非常长且枯燥. 如果了解源码的实现, 那么建议一定要连着看下去, 不然很容易思路就乱了. 如果只是想大致了解一下GestureDetector的实现过程, 那么可以直接跳到文章最后的总结.
手势的判断
前面我们说了Flutter的事件处理链和事件分发, 这里我们说一下Flutter的事件监听.
GestureDetector是Flutter为我们封装好的事件监听的功能类, 可以识别大多数的手势, 我们就以它为线索来学习Flutter的事件监听.
1 |
|
上面是GestureDetector的build函数, 中间有一大段生成gestures的内容, 主要是定义各种手势的, 我们先不看. 函数的最后是根据前面定义的手势和行为生成创建RawFestureDetector实例, 也就是说GestureDetector的功能主要是由RawGestureDetector完成的.
1 |
|
RawGestureDetector是一个StatefulWidget, 上面是它关联的State类的build函数. 可以看到, 它又是通过Listener来实现功能的.
1 | class Listener extends StatelessWidget { |
上面是Listener的源码, 结合源码给出的注释以及属性的名称, 我们可以知道, Listener就是用来监听DOWN、MOVE等原始事件的.
根据RawGestureDetector的build函数, 我们看到RawGestureDetector持有了一个监听DOWN事件的Listener. 这个可以理解, 因为DOWN事件是事件流的起始.
1 | void _handlePointerDown(PointerDownEvent event) { |
上面是RawGestureDetector在遇到DOWN事件的回调, _recognizers是GestureDetector定义的一系列手势. 那么这个函数的逻辑就很清楚了: 每次遇到一个事件流的开始, RawGestureDetector就将这个事件通知给所有的手势定义类中, 由它们进行后续的判定.
1 | void addPointer(PointerDownEvent event) { |
继续追踪addPointer函数, _pointToKind是一个Map, 用来存储输入设备, 不是我们关心的重点. isPointerAllowed函数用来判断手势是否接收这个事件, 接受与否要根据手势的定义以及手势当前的状态决定. 如果手势可以接收这个事件, 那么就调用addAllowedPointer函数来改变状态并继续追踪事件流; 否则调用handleNonAllowedPointer来拒绝这个事件. 这三个函数被不同的GestureRecognizer分别进行实现, 从而实现了不同手势的判断.
手势的竞争
只有手势的判断并不能完全解决问题, 很多手势都需要接收一系列的MOVE事件, 要想保证手势之间不发生冲突, 就要解决手势处理事件的优先级问题.
1 |
|
无论是什么GestureRecognizer, 最终都会在调用继承的OneSequenceGestureRecognizer类的startTrackingPointer函数. 顾名思义, 这个方法就是用来追踪事件流的. GestureBinding中的PointerRouter是一个Map, 它的key是事件流的标识pointer, value是手势的一系列回调. _trackedPointers是一个HashSet, 记录这个GestureRecognizer目前都正在追踪哪些事件流. _entries也是一个Map, key值同样是pointer, value值是一个GestureArenaEntry类. 如同HitTestEntry一样, GestureArenaEntry类也是一个封装类, 它由GestureArenaManager、pointer和GestureArenaMember组成. pointer是事件流的标识, GestureArenaMember是GestureRecognizer, 在GestureArenaEntry类里起始就是这个GestureRecognizer自己.
pointerRouter我们先不管, _trackedPointers.add(pointer)也很好理解, 我们先看最后的_addPointerToArena函数.
1 | GestureArenaEntry _addPointerToArena(int pointer) { |
GestureBinding的gestureArena是一个GestureArenaManager类. 它实际上就是维护了一个Map. 这个Map的key值仍然是GestureRecognizer正在追踪的事件流, value是一个_GestureArena类. 这个类也很简单, 就是维护一个GestureArenaMember的List. GestureBinding.instance.gestureArena.add这个函数的实现逻辑, 我们可以用下面的这个比喻去理解:
一个GestureRecognizer是一个竞技场斗士, 它正在追踪的事件流的编号可以理解为是这个斗士被分配的组, 每一组都是都有一个竞技场_GestureArena. GestureArenaManager是比赛的委员会, 它通过add函数来将每一位斗士按照被分配的组引导到对应的竞技场中. 在同一个竞技场中的都是需要进行比赛, 赢的人就可以获得事件的处理权.
但是委员会怎么知道冠军要怎么处理事件呢? 我们再看startTrackingPointer函数中的GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);这句话. 原来斗士要想进入竞技场, 就必须先把自己怎么处理事件告诉委员会才行.
接下来就是正式的竞争了.
1 | // from HitTestTarget |
由于GestureBinding是宏观上分发所有事件的角色, 所以我们还是要总它的handleEvent开始看. 从函数中可以看到, 一旦遇到了DOWN事件, 就会调用GestureArenaManager的close函数.
1 | void close(int pointer) { |
close函数的原理很简单, 首先判断这个事件流是不是正常的, 如果是就关闭竞技场并且开始竞争.
1 | void _tryToResolveArena(int pointer, _GestureArena state) { |
_tryToResolveArena的逻辑也很简单. 如果竞技场没有人, 那么直接关闭这个竞技场并返回. 如果竞技场只有一个人, 那么冠军就直接产生了. 如果竞技场有多个人, 那么就要解决优先级问题了. 解决优先级问题的方式也很简单——打假赛. 当eagerWinner不为空的时候, 说明这些GestureRecognizer中有一个高优先级的, 这种情况下就不用比赛了, 直接让他胜出就好了.
当然, 大多数情况下GestureRecognizer的优先级都是相同的. 这种情况也不需要我们特别操心, 因为目前只处理了DOWN事件, 通过后续的事件, 还是可以让对应手势的GestureRecognizer坚持到最后的.
总结
说了这么多, 可能大家关于Flutter的事件机制还是没有一个特别清楚的认识, 所以在这里我们再简单总结一下
GestureDetector定义了一系列手势. 由于所有的手势的第一个事件都是DOWN, 所以只有DOWN事件产生后, 所有手势的GestureRecognizer才会被激活.
GestureRecognizer被激活后, 首先要看是否存在高优先级的GestureRecognizer. 被激活的GestureRecognizer全部被加入到GestureBinding的gestureArena中. gestureArena通过事件流的唯一标识pointer来将被激活的GestureRecognizer进行分组
所有的Widget都继承自RenderBox类. 当DOWN事件产生时, 所有的Widget首先判断事件是否发生在自己的区域内, 如果是, 那么先通过递归判断自己子控件是否在监听事件, 如果有, 那么就将子控件和自己加入这个事件的处理链; 如果没有那么再看自己是不是在监听事件, 是的话就只将自己加入处理链. 这个过程结束后, 就会形成一条可能会响应该事件流的处理链. MOVE、UP、CANCEL事件都会复用DOWN事件产生的处理链.
事件链产生完毕后, Flutter认为所有被激活的GestureRecognizer都应该被分组了, 这时关闭对应的事件流的分组, 开始判断优先级.
少数情况下, 事件流只激活了一个GestureRecognizer或者激活了一个高优先级的GestureRecognizer, 亦或者没有激活GestureRecognizer, 这样就解决了同一时刻多个控件征用事件冲突的问题.
大多数情况下, 不存在事件冲突的问题. 事件流后续的事件通过每个GestureRecognizer的addPointer函数来判断是否符合手势定义. 如果是, 那么调用addAllowedPointer函数来响应事件; 否则调用addNonAllowedPointer函数来拒绝事件.
addAllowedPointer会根据接受的事件决定GestureRecognizer的新状态, 如果是手势的结束状态, 那么就接受这个手势, 并调用响应的回调; 否则继续等待下一个事件.