网络知识 娱乐 Flutter事件响应源码分析

Flutter事件响应源码分析

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类型的队列 
///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,其兄弟节点没有机会进行命中测试,这种遍历方式也可以叫做深度优先遍历。

如果需要兄弟节点也可以通过命中测试,可以参考 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,感兴趣的同学可以自己取看下对应的源码。

文/阿宝

关注得物技术,做最潮技术人!