Flutter作为一个UI框架,自身也有本人的事件处理形式,本文次要论述触摸事件从native传递到Flutter后是如何被widget辨认以及散发的。至于native零碎是如何监听触摸事件以及传递事件到Flutter,感兴趣的能够本人去理解下不同的宿主零碎解决的形式也是不同的。
事件处理流程
Flutter中对触摸事件的解决大抵能够分为以下几个阶段:
- 监听事件的到来
- 对widget是否能响应事件进行命中测试
- 将事件分发给通过命中测试的widget
后续将触摸事件间接称为event
监听事件
event是由native零碎通过音讯通道传递到Flutter中的,因而Flutter必然会有对应的监听办法或者回调,从Flutter启动流程的源码中能够在mixin GestureBinding查看到上面代码:
@override void initInstances() { super.initInstances(); _instance = this; window.onPointerDataPacket = _handlePointerDataPacket; }
其中window.onPointerDataPacket正是监听event的回调,window是 Flutter 连贯宿主操作系统的接口,其中蕴含了以后设施和零碎的一些信息以及Flutter Engine的一些回调,上面展现了其局部属性。其余属性能够自行查看官网文档,留神这里的window不是dart:html规范库里window 类。
class Window { // 以后设施的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示成果就越精密保真。 // DPI是设施屏幕的固件属性,如Nexus 6的屏幕DPI为3.5 double get devicePixelRatio => _devicePixelRatio; // Flutter UI绘制区域的大小 Size get physicalSize => _physicalSize; // 以后零碎默认的语言Locale Locale get locale; // 以后零碎字体缩放比例。 double get textScaleFactor => _textScaleFactor; // 当绘制区域大小扭转回调 VoidCallback get onMetricsChanged => _onMetricsChanged; // Locale发生变化回调 VoidCallback get onLocaleChanged => _onLocaleChanged; // 零碎字体缩放变动回调 VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; // 绘制前回调,个别会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用 FrameCallback get onBeginFrame => _onBeginFrame; // 绘制回调 VoidCallback get onDrawFrame => _onDrawFrame; // 点击或指针事件回调 PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket; // 调度Frame,该办法执行后,onBeginFrame和onDrawFrame将紧接着会在适合机会被调用, // 此办法会间接调用Flutter engine的Window_scheduleFrame办法 void scheduleFrame() native 'Window_scheduleFrame'; // 更新利用在GPU上的渲染,此办法会间接调用Flutter engine的Window_render办法 void render(Scene scene) native 'Window_render'; // 发送平台音讯 void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ; // 平台通道音讯解决回调 PlatformMessageCallback get onPlatformMessage => _onPlatformMessage; ... //其它属性及回调 }
当初咱们有了event在Flutter端的入口函数 _handlePointerDataPacket,通过这个函数咱们能够查看Flutter接管到event后是如何操作的,比较简单咱们间接看下代码。
_handlePointerDataPacket
将event做一次转换,而后增加到一个队列中
///_pendingPointerEvents: Queue<PointerEvent>类型的队列 ///locked: 通过标记位来实现的一个锁 void _handlePointerDataPacket(ui.PointerDataPacket packet) { // We convert pointer data to logical pixels so that e.g. the touch slop can be // defined in a device-independent manner. _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio)); if (!locked) _flushPointerEventQueue(); }
_flushPointerEventQueue
遍历下面的队列,locked能够了解为一个简略的信号量(锁),调用对应的handlePointerEvent,handlePointerEvent内间接调用_handlePointerEventImmediately办法。
void _flushPointerEventQueue() { assert(!locked); while (_pendingPointerEvents.isNotEmpty) handlePointerEvent(_pendingPointerEvents.removeFirst()); } ///handlePointerEvent :默认啥也没干就是调用了_handlePointerEventImmediately办法 ///简化后的代码 void handlePointerEvent(PointerEvent event) { _handlePointerEventImmediately(event); }
_handlePointerEventImmediately
外围办法:依据不同事件类型开启不同的流程,这里咱们只关怀PointerDownEvent事件。
能够看到当flutter监听到PointerDownEvent时,会对指定地位开启命中测试流程。
Flutter中蕴含多种事件类型:能够在lib->src->gesture->event.dart中查看具体信息
// PointerDownEvent: 手指在屏幕按下是产生的事件void _handlePointerEventImmediately(PointerEvent event) { HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {//down assert(!_hitTests.containsKey(event.pointer)); ///存储通过命中测试的widget hitTestResult = HitTestResult(); ///开始命中测试 hitTest(hitTestResult, event.position); ///测试实现后会将通过命中测试的后果寄存到一个全局map对象里 if (event is PointerDownEvent) { _hitTests[event.pointer] = hitTestResult; } } else if (event is PointerUpEvent || event is PointerCancelEvent) {//cancel hitTestResult = _hitTests.remove(event.pointer); } else if (event.down) {//move hitTestResult = _hitTests[event.pointer]; } if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); ///散发事件 dispatchEvent(event, hitTestResult); } }
本阶段次要内容:
- 注册了监听事件的回调:_handlePointerDataPacket
- 接管事件后,将转换后的事件放到一个queue中:_flushPointerEventQueue
- 遍历queue开始命中测试流程:_handlePointerEventImmediately-> hitTest(hitTestResult, event.position)
命中测试
目标是确定在给定的event的地位上有哪些渲染对象(renderObject),并且在这个过程中会将通过命中测试的对象寄存在上文中的HitTestResult对象中。 通过源码调用流程看下flutter外部是如何进行命中测试的,在这些流程中那些咱们是能够管制的。
筹备
开始命中测试源码剖析之前先看下上面的代码,这是Flutter入口函数main办法中调用runApp初始化的外围办法,这里WidgetsFlutterBinding 实现了多个mixin,而这些mixin中有多个都实现了hitTest办法,这种状况下离with关键字远的优先执行,所以在 _handlePointerEventImmediately中调用的hitTest办法是在RendererBinding中而不是GestureBinding。具体细节能够去理解下dart中with多个mixin且每个mixin中都蕴含同一个办法时的调用关系,简略说就是会先调用最初with的mixin。
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { static WidgetsBinding ensureInitialized() { if (WidgetsBinding.instance == null) WidgetsFlutterBinding(); return WidgetsBinding.instance!; } }
RendererBinding. hitTest: 命中测试的开始办法
次要作用是调用渲染树根节点的hitTest办法
@override void hitTest(HitTestResult result, Offset position) { assert(renderView != null); assert(result != null); assert(position != null); /// renderView:渲染树根节点,继承自RenderObject renderView.hitTest(result, position: position); super.hitTest(result, position); }
RendererBinding.renderView:
渲染树的根节点
/// The render tree that's attached to the output surface. RenderView get renderView => _pipelineOwner.rootNode! as RenderView; /// Sets the given [RenderView] object (which must not be null), and its tree, to /// be the new render tree to display. The previous tree, if any, is detached. set renderView(RenderView value) { assert(value != null); _pipelineOwner.rootNode = value; }
RenderView.hitTest
根节点的hitTest办法实现中有两个留神点:
- 根节点必然会被增加到HitTestResult中,默认通过命中测试
- 从这里开始上面的调用流程就是和child类型相干了
<!---->
- child重写了hitTest调用重写后的办法
- child没有重写则调用父类RenderBox的默认实现
bool hitTest(HitTestResult result, { required Offset position }) { ///child是一个 RenderObject 对象 if (child != null) child!.hitTest(BoxHitTestResult.wrap(result), position: position); result.add(HitTestEntry(this)); return true; }
RenderBox.hitTest
默认实现的办法,如果child没有重写则会调用到此办法,外部次要蕴含上面两个办法的调用:
- hitTestChildren性能是判断是否有子节点通过了命中测试,如果有,则会将子组件增加到 HitTestResult 中同时返回 true;如果没有则间接返回false。该办法中会递归调用子组件的 hitTest 办法。
- hitTestSelf() 决定本身是否通过命中测试,如果节点须要确保本身肯定能响应事件能够重写此函数并返回true ,相当于“强行申明”本人通过了命中测试。
/// 移除了断言后的代码 bool hitTest(BoxHitTestResult result, { required Offset position }) { if (_size!.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry(this, position)); return true; } } return false; } /// RenderBox中默认实现都是返回的false @protected bool hitTestSelf(Offset position) => false; @protected bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
重写hitTest:
在这个例子里,咱们自定义一个widget,重写其hitTest办法,看下调用流程。
void main() { runApp( MyAPP()); } class MyAPP extends StatelessWidget { const MyAPP({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container( child: DuTestListener(), ); } } class DuTestListener extends SingleChildRenderObjectWidget { DuTestListener({Key? key, this.onPointerDown, Widget? child}) : super(key: key, child: child); final PointerDownEventListener? onPointerDown; @override RenderObject createRenderObject(BuildContext context) => DuTestRenderObject()..onPointerDown = onPointerDown; @override void updateRenderObject( BuildContext context, DuTestRenderObject renderObject) { renderObject.onPointerDown = onPointerDown; } } class DuTestRenderObject extends RenderProxyBox { PointerDownEventListener? onPointerDown; @override bool hitTestSelf(Offset position) => true; //始终通过命中测试 @override void handleEvent(PointerEvent event, covariant HitTestEntry entry) { //事件散发时处理事件 if (event is PointerDownEvent) onPointerDown?.call(event); } @override bool hitTest(BoxHitTestResult result, {required Offset position}) { // TODO: implement hitTest print('ss'); result.add(BoxHitTestEntry(this, position)); return true; } }
点击屏幕(彩色的)展现上面的调用栈:
子类重写HitTest后,在RenderView后,间接调用了咱们重载的hitTest办法,齐全印证了咱们下面剖析的逻辑
罕用widget剖析
本节来剖析下Flutter中的Center、Column,看下Flutter是如何解决child和children两种类型的hitTest.
Center
继承:Center->Align->SingleChildRenderObjectWidget
在Align中重写createRenderObject 返回RenderPositionedBox类。RenderPositionedBox自身没有重写hitTest办法,但在其父类的父类RenderShiftedBox中重写了hitTestChildren办法
hitTestChildren
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { if (child != null) { ///父组件在传递束缚到子widget时,会计算一些子widget在父widget中的偏移,这些数据通常存在BoxParentData中 ///这里就应用子widget在父widget中的偏移 final BoxParentData childParentData = child!.parentData! as BoxParentData; return result.addWithPaintOffset( offset: childParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset? transformed) { assert(transformed == position - childParentData.offset); ///递归调用child的hitTest办法 ///transformed转换后的地位 return child!.hitTest(result, position: transformed!); }, ); } return false; } addWithPaintOffset bool addWithPaintOffset({ required Offset? offset, required Offset position, required BoxHitTest hitTest, }) { ///做一些坐标转换 final Offset transformedPosition = offset == null ? position : position - offset; if (offset != null) { pushOffset(-offset); } ///回调callBack final bool isHit = hitTest(this, transformedPosition); if (offset != null) { popTransform(); } return isHit; }
将下面示例中MyApp中的build换成上面代码,在来看下调用栈
@override Widget build(BuildContext context) { return Container( child: Center(child: DuTestListener()), ); }
调用栈:
很清晰,因为Center相干父类没有重写hitTest办法,所以renderView中间接调用基类RenderBox中的hitTest,这个hitTest中又调用了被重写的hitTestChildren,在hitTestChildren中通过递归的形式对widget进行命中测试。
Column
继承:Column->Flex->MultiChildRenderObjectWidget
RenderFlex在Flex中重写createRenderObject返回RenderFlex,RenderFlex自身没有重写hitTest办法,而是重写了hitTestChildren办法
hitTestChildren
外部间接调用了RenderBoxContainerDefaultsMixin.defaultHitTestChildren办法
@override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { return defaultHitTestChildren(result, position: position); } RenderBoxContainerDefaultsMixin.defaultHitTestChildren bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) { // The x, y parameters have the top left of the node's box as the origin. ChildType? child = lastChild; while (child != null) { final ParentDataType childParentData = child.parentData! as ParentDataType; final bool isHit = result.addWithPaintOffset( offset: childParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset? transformed) { assert(transformed == position - childParentData.offset); return child!.hitTest(result, position: transformed!); }, ); if (isHit) return true; child = childParentData.previousSibling; } return false; }
Center和Colunm一个是蕴含单个widget,一个蕴含多个widget,而且都是重写了hitTestChildren办法来管制命中测试,两者次要区别就在于Colunm的hitTestChildren应用了while循环来遍历本人的子widget进行命中测试。而且Colunm遍历程序是先遍历lastchild,如果lastchild没有通过命中测试,则会持续遍历它的兄弟节点,如果lastchild通过命中测试,这间接return true,其兄弟节点没有机会进行命中测试,这种遍历形式也能够叫做深度优先遍历。
如果须要兄弟节点也能够通过命中测试,能够参考<Flutter实战> 8.3节的形容,这里不在开展
将下面事例中MyApp中的build换成上面代码,在来看下调用栈
@override Widget build(BuildContext context) { return Container( child: Column( children: [ DuTestListener(), DuTestListener() ], ) ); }
调用栈
尽管咱们蕴含了两个DuTestListener,然而最终只会调用一次DuTestListener的hitTest办法,就是因为lastChid曾经通过命中测试,它的兄弟节点没有机会进行命中测试了。
流程图:
命中测试小结:
- 从Render Tree的节点开始向下遍历子树
- 遍历的形式:深度优先遍历
- 能够通过重写hitTest、hitTestChildren、hitTestSelf来自定义命中测试相干的操作
- 存在兄弟节点时,从最初一个开始遍历,任何一个通过命中测试,则终止遍历,未遍历的兄弟节点没有机会在参加。
- 深度优先遍历的过程会先对子widget进行命中测试,因而子widget会先于父widget增加到BoxHitTestResult中。
- 所有通过命中测试的widget会被增加到BoxHitTestResult内一个数组中,用于事件散发。
留神:hitTest办法的返回值不会影响是否通过命中测试,只有被增加到BoxHitTestResult中的widget才是通过命中测试的。
事件散发
实现所有节点的命中测试后,代码返回到GestureBinding._handlePointerEventImmediately,将通过命中测试的hitTestResult存储在一个全局的Map对象 _hitTests里,key为event.pointer, 而后调用 dispatchEvent办法进行事件散发。
GestrueBinding.dispatchEvent
///精简后的代码 void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); if (hitTestResult == null) { assert(event is PointerAddedEvent || event is PointerRemovedEvent); pointerRouter.route(event); return; } for (final HitTestEntry entry in hitTestResult.path) { entry.target.handleEvent(event.transformed(entry.transform), entry); } }
通过源码能够看到dispatchEvent函数的的作用就是遍历通过命中测试的节点,而后调用对应的handleEvent办法,子类能够重写handleEvent办法来监听事件的散发。
依然以下面的代码为例看下调用栈:
和咱们想的统一从dispatchEvent办法开始,调用咱们自定义的widget中的handleEvent。
小结:
- 事件散发没有终止条件,只有在通过命中测试的点,都会被依照退出程序散发事件
- 子widget的散发先于父widget
总结
本文次要通过源码的调用流程联合一些简略的事例来剖析flutter中事件的响应原理,这里探讨的只是最根底的事件处理流程,Flutter在这些根底流程上封装了事件监听、手势解决以及层叠组件这些更加语义化的widget,感兴趣的同学能够本人取看下对应的源码。
文/阿宝
关注得物技术,做最潮技术人!