Flutter event response source code analysis

As a UI framework, Flutter itself has its own event processing method. This article mainly describes how touch events are recognized and distributed by widgets after they are passed from native to Flutter. As for how the native system listens for touch events and transmits events to Flutter, those who are interested can learn about how different host systems handle it differently.

event handling process

The processing of touch events in Flutter can be roughly divided into the following stages:

  • Listen for the arrival of events
  • Hit test whether the widget can respond to events
  • Distribute events to widgets that pass the hit test

Subsequent touch events are directly referred to as events

listen event

The event is passed to Flutter by the native system through the message channel, so Flutter must have a corresponding monitoring method or callback. From the source code of the Flutter startup process, you can view the following code in the mixin GestureBinding :

@override 
void initInstances() { 
  super.initInstances(); 
  _instance = this; 
  window.onPointerDataPacket = _handlePointerDataPacket; 
} 
复制代码

Among them, window.onPointerDataPacket is the callback to monitor the event, and window is the interface that Flutter connects to the host operating system, which contains some information about the current device and system and some callbacks of the Flutter Engine. Some of its properties are shown below. For other properties, you can check the official documentation by yourself. Note that the window here is not the window class in the dart:html standard library.

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; 
   
  ... //其它属性及回调 
    
} 
复制代码

Now we have the entry function _handlePointerDataPacket of the event on the Flutter side . Through this function, we can see how Flutter operates after receiving the event. It is relatively simple to look at the code directly.

_handlePointerDataPacket

Convert the event once and add it to a queue

///_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,感兴趣的同学可以自己取看下对应的源码。

文/阿宝 关注得物技术,做最潮技术人!

Guess you like

Origin juejin.im/post/7078119087540764709
Recommended