本文共 7098 字,大约阅读时间需要 23 分钟。
Flutter中,当点击事件到来时,第一个回调的方法就是GestureBinding#_handlePointerDataPacket(); 它是通过window的onPointerDataPacket()方法添加的。
//GestureBinding#_handlePointerDataPacket(); void _handlePointerDataPacket(ui.PointerDataPacket packet) { //该方法的逻辑我们不需要理会,只需要知道这个方法的结果是什么就行了。 //该方法的目的是把原生系统传递过来的点击事件序列,转换成Flutter的事件对象。 //虽然该方法的转换逻辑我们不需要弄懂,但是粗略一看还是能发现:Flutter把原生传递过来的点击事件序列 根据不同的事件类型转换成不同的PointEvent子类。 _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio)); //开始对转换后的事件序列进行处理 if (!locked) _flushPointerEventQueue(); }
从源码我们知道,Flutter接收到原生的事件序列之后,首先把原生的事件序列转换成Flutter中的事件对象。Flutter把事件类型转换成这几类事件:
//GestureBinding# _flushPointerEventQueue()void _flushPointerEventQueue() { //循环转换后的事件序列,每次循环都从头部取出事件序列。 while (_pendingPointerEvents.isNotEmpty){ //从头部取出事件序列,进行处理。 handlePointerEvent(_pendingPointerEvents.removeFirst()); } }
handlePointerEvent()方法中每次从头部取出事件序列进行处理。handlePointerEvent()中内部其实是调用" _handlePointerEventImmediately() "方法进行事件处理的,我们看该方法的源码:
//GestureBinding# _handlePointerEventImmediately()void _handlePointerEventImmediately(PointerEvent event) { HitTestResult? hitTestResult; //Flutter认为down事件,Signal事件,Hover事件都能表示“这是一个事件序列的开始,也是事件序列的第一个事件” if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { //创建HitTestResult,用于储存命中测试的结果。 hitTestResult = HitTestResult(); //hitText()开启查找响应事件序列的RenderObject的流程,即命中的RendObject。并且记录下从响应事件序列的RenderObject开始到根RenderView节点 这条链路上的所有RenderObject节点。 hitTest(hitTestResult, event.position); //保存查找后的结果。 if (event is PointerDownEvent) { _hitTests[event.pointer] = hitTestResult; } } else if (event is PointerUpEvent || event is PointerCancelEvent) { //Flutter认为up事件,cancel事件是表示“这是一个事件序列的结束,也是事件序列最后一个事件” //事件序列结束,移除所有响应事件的组件。并且进行最后一次事件响应,即dispatchEvent()调用最后这一次。 hitTestResult = _hitTests.remove(event.pointer); } else if (event.down) { //event.down属性表示 当前的事件中该手指是按在屏幕上的。这是一个事件序列中的某个事件,拿着之前命中测试的结果去处理该事件 hitTestResult = _hitTests[event.pointer]; } //当hitTestResult不为null,就进行事件分发。 if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) { //执行事件分发的操作。 dispatchEvent(event, hitTestResult); } }
_handlePointerEventImmediately()其实做了两件事:
另外,_handlePointerEventImmediately()方法的源码能看出:Flutter是认为"down事件,Signal事件,Hover事件"都能标志着是"这是一个事件序列的开始",认为"up事件,cancel事件"都能标志着"这是一个事件序列的结束"
我们先看步骤(1)是如果查找响应的RenderObject的(即命中的RenderObject)的 ?。我们看看RendererBinding#hitTest()的处理过程。为什么是看RendererBinding#hitTest()?因为Dart语法"混入特性",后面的混入类会覆盖前面混入类的分发,因此GestureBinding#hitTest()方法被RendererBinding#hitTest()方法覆盖了。
//RendererBinding#hitTest() @overridevoid hitTest(HitTestResult result, Offset position) { //直接把事件从根RenderView开始向下传递。 renderView.hitTest(result, position: position); //然后调用GestureBinding的hitTest方法,它的hitTest()方法只是把GestureBinding加入HitTestResult的path中。但它不是第一个加入的,因为前面renderView.hitTest()方法早就把子节点以及其自身加入到HitTestResult的path中了。 //最后HitTestResult的path集合中的元素是将会是按照:从叶子节点到根节点的顺序排列的。 super.hitTest(result, position);}------------//GestureBinding#hitTest()@override // from HitTestablevoid hitTest(HitTestResult result, Offset position) { result.add(HitTestEntry(this));}
hitTest()方法是每一个RenderObject都会去实现的,hitTest()方法表示事件被传递给当前RenderObject处理,通常RenderObject是怎么处理事件的呢?其实想怎么处理都可以的,哪怕空实现也行,因为事件只要到达了RenderObject,RenderObject有很大的自主选择,可以选择处理或者不处理事件,可以选择向下传递或者不传递事件,还可以选择是否把自己加入HitTestResult的path中(如果选择不加入,那么意味着后续事件不会进入当前RenderObject)。基类RenderBox默认帮我们实现了hitTest()的逻辑。我们看看基类RenderBox默认实现的逻辑。
//RendreBox#hitTest();bool hitTest(BoxHitTestResult result, { required Offset position }) { //RenderBox默认的实现逻辑是: //判断当前的点击坐标是否在组件内。 if (_size!.contains(position)) { //判断Children是否响应该事件,再判断自身是否响应该事件 if (hitTestChildren(result, position: position) || hitTestSelf(position)) { //不管是Children响应还是自身响应,都把自己加入HitTestResult的path中,进入HitTestResult的path中,则当前的RenderObject将会接收后续的事件。 result.add(BoxHitTestEntry(this, position)); return true; } } return false;}
我们可以看到默认RenderBox#hitTest()中做的事情:
虽然官方在基类RenderBox中默认帮我们实现了hitTest();的事件处理逻辑
,而且我们是可以通过子类重写的方式把基类RenderBox中hitTest的默认逻辑覆盖的
,但是建议如果没有特殊需要,就不用重写hitTest()方法,使用官方默认的实现就很好的了
。
那如何判断Children是否响应事件呢?很简单的,其实是Children判断它自身是否响应事件。首先hitTestChildren方法在基类RenderBox中是默认返回false的,如果当前RenderObject是有子节点Children的,那么当前RenderObject就要重写hitTestChildren()方法,在该方法内部调用子节点Children的hitTest();把事件传递给子节点Children,Children就会在它自己的hitTest()中判断是否要响应事件
。那Children具体是如何判断的呢?Children它同样是一个RenderObject,同样有很大的自主选择权,因为每个Children都是不同的,在判断逻辑上就不可能统一成一个逻辑,因此Children具体如何判断是否响应事件是由Children自己决定的,
(例如,ListView和Text两个组件在判断是否响应事件的逻辑上,就是不同的)。在这种情况下,我们仍然是推荐Children的hitTest()方法中的逻辑按照按照上面(步骤1~5)的判断操作进行
,即按照步骤1~步骤5判断,并且把自身加入HitTestResult的path中
。最后Children判断完成后会返回true或false告知父节点判断结果。
到此,我们知道:Children响应事件时会把自身加入HitTestResult的path中,父节点自身或者父节点的Children节点响应事件的话,父节点会把自身加入HitTestResult的path中。
导致最后 HitTestResult的path中元素就是会按照:从叶子节点 到 根RenderView的顺序排列。HitTestResult的path中的元素就是从叶子节点 到 根RenderView的链路路径上所有的节点。
回过去重新看_handlePointerEventImmediately()方法:在事件序列的开始(即down事件,Signal事件,Hover事件)时,进hitTest命中测试,把命中测试的结果保存在hitTests这个map集合中;当事件序列后续事件到来时,直接从hitTests集合里取出“命中结果”来响应事件。等事件序列的结束事件(即up事件,cancel事件)到来时,移除hitTests集合里缓存的“命中结果”。
我们看看对于事件序列的后续事件是怎么进行分发的。由dispatchEvent()进行后续事件分发:
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); //hitTestResult==null,表示没有找到响应该事件的组件。即没有命中。 //没有命中的话,代表这个事件是 add,remove,这两种事件。 if (hitTestResult == null) { assert(event is PointerAddedEvent || event is PointerRemovedEvent); try { //交给注册路由的回调处理 pointerRouter.route(event); } catch (exception, stack) { //省略错误处理逻辑,我们就看主体逻辑,至于错误处理 我们知道有这么一个处理即可 } return; } //命中的话,循环hitTestResult的path中所保存的所有RenderObject节点。 for (final HitTestEntry entry in hitTestResult.path) { try { //依次调用 handlerEvent()进行事件处理。 entry.target.handleEvent(event.transformed(entry.transform), entry); } catch (exception, stack) { //省略错误处理逻辑,我们就看主体逻辑,至于错误处理 我们知道有这么一个处理即可 } }}
handleEvent();是一个抽象方法,如果处理事件是由具体的子类去实现的。从源码发现:循环的是hitTestResult的path集合,而path集合中 的元素是 按照:从叶子节点 到 根RenderView的顺序排列。由此可得:(1)先把事件传递给叶子节点,然后一层层循环直到 根RenderView。(2)因为是path集合中的元素,循环过程中也没有提供任何中断循环的方法或接口,路径上所有的节点都会因为循环而收到事件序列,每个节点都能消耗点击事件。
转载地址:http://tlkni.baihongyu.com/