乐趣区

关于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<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,感兴趣的同学能够本人取看下对应的源码。

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

退出移动版