博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Flutter 事件分发流程
阅读量:4074 次
发布时间:2019-05-25

本文共 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把事件类型转换成这几类事件:

  • 事件类型 :ui.PointerChange.add。代表设备已开始跟踪指针。指针可能悬停在设备上方,尚未与设备表面接触。事件对应PointerAddedEvent类。
  • 事件类型:ui.PointerChange.hover。代表手指相对于设备移动,但未与设备接触。事件对应PointerHoverEvent类。
  • 事件类型:ui.PointerChange.down。代表指针已经与设备接触。事件对应PointerDownEvent类。
  • 事件类型:ui.PointerChange.move。代表指针在设备表明移动。事件对应PointerMoveEvent类。
  • 事件类型:ui.PointerChange.up。代表指针已停止与设备接触。事件对应PointerUpEvent类。
  • 事件类型:ui.PointerChange.cancel。指针的输入不在执行某个接收器。事件对应PointerCancelEvent类。
  • 事件类型:ui.PointerChange.remove。代表设备不再跟踪指针。可能是指针已偏离设备的悬停检测范围,也可能是设备已与系统完全断开连接。事件对应PointerRemovedEvent类。
//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()其实做了两件事:

  • (1)确定当前事件是命中哪些RenderObject,记录下从该RendObject 到 根RenderView 的链路上所有的RenderObject节点。
  • (2)把事件分发给 步骤(1)中记录的链路上所有的RenderObject。分发的时候先发给叶子节点,从叶子节点开始向上分发事件一直到根RenderView。

另外,_handlePointerEventImmediately()方法的源码能看出:Flutter是认为"down事件,Signal事件,Hover事件"都能标志着是"这是一个事件序列的开始",认为"up事件,cancel事件"都能标志着"这是一个事件序列的结束"

hitTest()查找响应事件的RenderObject

我们先看步骤(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()中做的事情:

  1. 判断坐标是否在当前RenderObject所代表的组件内,是的话进行后续逻辑,
  2. 判断Children是否响应事件,
  3. 判断自身是否响应事件,
  4. 步骤2和步骤3只要满足一个,就把自己加入HitTestResult的path中,然后返回true,表示响应该事件。
  5. 步骤1,2,3都不满足。那返回false,表示不会响应该事件;

虽然官方在基类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的链路路径上所有的节点。

dispatchEvent()进行后续事件分发

回过去重新看_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/

你可能感兴趣的文章
springboot2 集成Hibernate JPA 用 声明式事物
查看>>
fhs-framework jetcache 缓存维护之自动清除缓存
查看>>
SpringBoot 动态编译 JAVA class 解决 jar in jar 的依赖问题
查看>>
fhs-framework springboot mybatis 解决表关联查询问题的关键方案-翻译服务
查看>>
ZUUL2 使用场景
查看>>
Spring AOP + Redis + 注解实现redis 分布式锁
查看>>
支付宝生活号服务号 用户信息获取 oauth2 登录对接 springboot java
查看>>
CodeForces #196(Div. 2) 337D Book of Evil (树形dp)
查看>>
uva 12260 - Free Goodies (dp,贪心 | 好题)
查看>>
uva-1427 Parade (单调队列优化dp)
查看>>
【设计模式】学习笔记14:状态模式(State)
查看>>
poj 1976 A Mini Locomotive (dp 二维01背包)
查看>>
斯坦福大学机器学习——因子分析(Factor analysis)
查看>>
项目导入时报错:The import javax.servlet.http.HttpServletRequest cannot be resolved
查看>>
linux对于没有写权限的文件如何保存退出vim
查看>>
Windows下安装ElasticSearch6.3.1以及ElasticSearch6.3.1的Head插件
查看>>
IntelliJ IDEA 下的svn配置及使用的非常详细的图文总结
查看>>
【IntelliJ IDEA】idea导入项目只显示项目中的文件,不显示项目结构
查看>>
ssh 如何方便的切换到其他节点??
查看>>
JSP中文乱码总结
查看>>