Flutter 源码梳理系列(四十四):HitTesting:Coordinate-space Transformations two

GestureBinding._flushPointerEventQueue

 接上篇,_handlePointerDataPacket 函数内会进行 PointerData 的数据处理,会把入参传递来的 ui.PointerDataPacket packet 参数的 data 属性:final List data 列表中的 PointerData 转换为对应类型的 PointerEvent,目前通过打印看到实际是转换了两个 PointerEvent 的子类实例对象:PointerAddedEvent 和 PointerDownEvent,并且它们两个的 postion 属性值都是:Offset(194.7, 163.7)

 当把点击事件的数据转化为 PointerEvent 并收集在 _pendingPointerEvents 队列中,接下来便是循环遍历此队列中的 PointerEvent 对象了,即开始执行 _flushPointerEventQueue 函数。

  void _flushPointerEventQueue() {
  
    
    while (_pendingPointerEvents.isNotEmpty) {
      
      
      handlePointerEvent(_pendingPointerEvents.removeFirst());
    }
  }

 在 _flushPointerEventQueue 内会对 _pendingPointerEvents 队列中的 PointerEvent 从头开始进行处理,在我们的实例代码中,看到第一个被处理的是 PointerDownEvent 实例对象。

GestureBinding.handlePointerEvent & GestureBinding._handlePointerEventImmediately

 实际 handlePointerEvent 只是 _handlePointerEventImmediately 函数的超简单包装,在其内部添加了一些 debug 下的测试代码。

 handlePointerEvent 函数的官方注释是:将事件(PointerEvent event)分派给 hit test 在其 position 上找到的 target。这个方法根据事件类型(不同的 PointerEvent 子类,是不同的事件类型)将给定的事件发送给 dispatchEvent:(即:void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) 函数,hit test 结束后找到了 target,然后带着参数调用 dispatchEvent 函数。)。

  void handlePointerEvent(PointerEvent event) {
    
    if (resamplingEnabled) {
      _resampler.addOrDispatch(event);
      _resampler.sample(samplingOffset, samplingClock);
      return;
    }

    
    _resampler.stop();
    
    
    
    
    _handlePointerEventImmediately(event);
  }

 实际直接调用 _handlePointerEventImmediately 函数,注意当前的 event 参数的类型是:PointerDownEvent。所以本次执行的是 _handlePointerEventImmediately 函数中第一个 if 中的函数,首先创建一个 HitTestResult 实例对象,(它会在接下来的整个 hit testing 过程中使用,并最终记录一组 HitTestEntry 对象。)然后是 hitTestInView 的调用,由此开始发起整个 hit testing 的过程。

 注意当这里调用 hitTestInView 函数时,会发现调用的是:RendererBinding.hitTestInView,那么这里为什么由 GestureBinding 转移到了 RendererBinding 中去了呢?首先我们看一下 GestureBinding、RendererBinding 和 WidgetsFlutterBinding 的定义:

 GestureBinding 定义,直接继承自 BindingBase 并实现了 HitTestable、HitTestDispatcher、HitTestTarget。

mixin GestureBinding on BindingBase implements HitTestable, 
                                               HitTestDispatcher,
                                               HitTestTarget { 

 RendererBinding 定义:

mixin RendererBinding on BindingBase,
                         ServicesBinding,
                         SchedulerBinding, 
                         GestureBinding, 
                         SemanticsBinding, 
                         HitTestable { 

 WidgetsFlutterBinding 定义:

class WidgetsFlutterBinding extends BindingBase with GestureBinding,
                                                     SchedulerBinding,
                                                     ServicesBinding, 
                                                     PaintingBinding, 
                                                     SemanticsBinding, 
                                                     RendererBinding, 
                                                     WidgetsBinding { 

 可以看到 WidgetsFlutterBinding 是 RendererBinding 的子类,而 RendererBinding 又是 GestureBinding 的子类,并且它重写了父类的 hitTestInView 函数。而在 Threads & Varibles 选项卡中看到当前的 this 指针指向正是一个 WidgetsFlutterBinding 对象,所以当它要执行 hitTestInView 函数时,必定是执行距离自己最近的父类,那么便是 RendererBinding 了,所以这里由 GestureBinding._handlePointerEventImmediately 执行到了 RendererBinding.hitTestinView 中。

 注意看如果本次是一个 PointerDownEvent/PointerPanZoomStartEvent 事件的话,则会把本次的 hit testing 的结果缓存下来,直接把 hitTestResult 实例对象记录在 GestureBinding 的 final Map _hitTests = {} 属性中,它会以本次 event 的 pointer 为 key,以本次 hit testing 的结果 hitTestResult 对象为 value 保存在这个 _hitTests Map 中。

  void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    
    if (event is PointerDownEvent ||
        event is PointerSignalEvent ||
        event is PointerHoverEvent ||
        event is PointerPanZoomStartEvent) {
        
      
      hitTestResult = HitTestResult();
      
      
      hitTestInView(hitTestResult, event.position, event.viewId);
      
      if (event is PointerDownEvent ||
          event is PointerPanZoomStartEvent) {
        
        
        
        _hitTests[event.pointer] = hitTestResult;
      }
    } else if (event is PointerUpEvent ||
               event is PointerCancelEvent ||
               event is PointerPanZoomEndEvent) {
               
      
      
      hitTestResult = _hitTests.remove(event.pointer);
      
    } else if (event.down ||
               event is PointerPanZoomUpdateEvent) {
      
      
      
      
      
      hitTestResult = _hitTests[event.pointer];
    }
    
    
    

    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
       
      
      dispatchEvent(event, hitTestResult);
    }
  }

 OK,下面我们开始看 RendererBinding.hitTestInView 中超长的 hitTest 递归调用,先看如何把点击事件的起始坐标:Offset(194.7, 163.7) 转换为目标 RenderBox 的坐标的。

RendererBinding.hitTestInView

 首先是看一下调用 hitTestInView 时传入的三个参数:

  • HitTestResult result: 传入一个刚刚创建的空的 HitTestResult 实例对象。
  • Offset position: 传入点击发生时的坐标点,x 和 y 坐标值已经由屏幕的物理像素为单位转换为逻辑像素为单位。
  • int viewId: 指定的 View ID,目前传入的是 0。

 viewId 用于从 RendererBinding 的 _viewIdToRenderView 属性中找到与这个 viewId 对应的 RenderView 实例对象。在本次调用这里看到 _viewIdToRenderView[viewId] 取得的是我们的 Render Tree 的根节点:_ReusableRenderView 实例对象。由此三个参数可得,hit testing 的三个开始条件就达成了:

  1. 一个 HitTestResult 空的实例对象,用于记录 hit test 的过程。
  2. 一个 Offset 当前点击发生时在当前屏幕坐标系的坐标点,并且已经把坐标值的单位转换为逻辑像素,这里可以使得不同物理分辨率的设备都能取得相同的逻辑坐标。
  3. Render Tree 的根节点。
final Map<Object, RenderView> _viewIdToRenderView = <Object, RenderView>{}` 

 在 RendererBinding.hitTestInView 中首先是根据入参 int viewId 取 view,通过 Threads & Variables 选项看到当前 this 指针指向的是 WidgetsFlutterBinding 单例对象,然后它的 _viewIdToRenderView 属性有值,是一个 size 是 1 的 _Map,而这个 Map 仅有的一个元素是:key 是 0,value 是 _ReusableRenderView(Render Tree 根节点)。

截屏2024-09-28 12.04.47.png

 然后是 RendererBinding.hitTestInView 函数的实现,此函数是重写自父类。看到实际仅有两行:_viewIdToRenderView[viewId] 取得 Render tree 的根节点,然后开始调用它的 hitTest 函数,开启整个 hit Testing 过程,而入参仅是最简单的空的 HitTestResult 实例对象和一个点击起点在当前屏幕的逻辑坐标。

  @override
  void hitTestInView(HitTestResult result, Offset position, int viewId) {
  
    
    _viewIdToRenderView[viewId]?.hitTest(result, position: position);
    
    
    super.hitTestInView(result, position, viewId);
  }

 这里随着 RendererBinding 的继承链去看的话,找到 RendererBing 中 super 的指向,其实是 GestureBinding,即这里的 super.hitTestInView(result, position, viewId) 的调用其实调用的是 GestureBinding.hitTestInView 函数,而它的实现贼简单,仅仅是把当前的 GestureBinding 对象构建一个 HitTestEntry 对象添加到当前的 HitTestResult result 实例对象的 _path 属性中。

  @override 
  void hitTestInView(HitTestResult result, Offset position, int viewId) {
  
    
    result.add(HitTestEntry(this));
  }

 通过之前对 HitTestEntry 的学习,我们已知能作为 HitTestEntry 构建参数的值必须是 HitTestTarget 的子类对象,而我们全局搜索发现仅有:RenderObject、GestureBinding、TextSpan 继承自 HitTestTarget,所以这里只有它们三个或者其子类实例对象才可用于构建 HitTestEntry 实例对象。而被 result 收集的 HitTestEntry 实例对象,则会在 hit test 执行结束时被调用其 handleEvent 函数进行调度 PointerEvent 事件。

 下面👇是 RenderView.hitTest 调用过程。

RenderView.hitTest

 首先在上面的函数堆栈追踪过程中记得 _viewIdToRenderView[viewId] 取到的 Render Tree 根节点是 _ReusableRenderView 实例对象,那么这里怎么调用到 RenderView.hitTest 这里了呢?是因为 _ReusableRenderView 是 RenderView 的子类,并且它没有重写 RenderView 的 hitTest 函数,所以这里的 _ReusableRenderView 实例对象调用 hitTest 函数时,其实是执行的自己的父类 RenderView 的 hitTest 函数。

 hitTest 函数用于确定给定位置 Offset position 入参处的 RenderObject 集合。如果给定的 position 坐标包含在此 RenderObject 或其子级之一中,则返回 true。将包含该 position 的任何 RenderObject 添加到给定的 HitTestResult result 入参中。position 参数位于 RenderView 的坐标系中,也就是逻辑像素中。这不一定是根 Layer 期望的坐标系,后者通常是物理(设备)像素。

  bool hitTest(HitTestResult result, { required Offset position }) {
  
    
    
    
    
    
    if (child != null) {
    
      
      child!.hitTest(BoxHitTestResult.wrap(result), position: position);
    }
    
    
    
    result.add(HitTestEntry(this));
    
    
    return true;
  }

 我们知道 Render Tree 的根节点的 child 是绝对不可能为 null 的,所以这里的 child!.hitTest(BoxHitTestResult.wrap(result), position: position) 继续往下执行,那么会执行向何方呢?通过 Threads & Variables 选项卡可以看到当前 _ReusableRenderView 对象的 child 属性是一个 RenderSemanticsAnnotations 实例对象,看它的定义看到是一个直接继承自 RenderProxyBox 的一个子类:


class RenderSemanticsAnnotations extends RenderProxyBox { 

 RenderSemanticsAnnotations 并没有重写 hitTest 函数,继续向下看它的父类 RenderProxyBox 的定义。

 RenderProxyBox 是一个用于 RenderBox 的基类,其外观类似于其 child。RenderProxyBox 具有单个 child,并通过调用 child 的 render box protocol 中的每个函数来模仿该 child 的所有属性。例如,RenderProxyBox 通过使用相同的约束要求其 child 进行布局然后匹配尺寸来确定其大小。RenderProxyBox 本身并不实用,因为你可能会更好地用其 child 替换 RenderProxyBox。然而,RenderProxyBox 是一个有用的基类,用于希望模仿其 child 大部分但非全部属性的 RenderBox。

 看下面👇 RenderProxyBox 的定义的全部内容,可以看到它自己仅是一个空类,它的全部内容是来自父类 RenderBox 和混入的 RenderObjectWithChildMixinRenderProxyBoxMixin

 而混入的 RenderObjectWithChildMixin 也指明了,此 RenderProxyBox 仅有一个子级,或者说是 RenderProxyBox 的子类也是仅有一个子级的存在。

 然后分别点入 RenderObjectWithChildMixin 和 RenderProxyBoxMixin 内部看到它们仅仅是自定义自己设定下的功能,而它们自己并没有重写 hitTest 函数,所以当这里的 RenderSemanticsAnnotations 实例对象调用 hitTest 函数时,其实是执行到了 RenderBox.hitTest 函数中。

class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {

  
  
  
  RenderProxyBox([RenderBox? child]) {
    this.child = child;
  }
}

mixin RenderObjectWithChildMixinextends RenderObject> on RenderObject { 
mixin RenderProxyBoxMixinextends RenderBox> on RenderBox, RenderObjectWithChildMixin { 

 RenderProxyBox 直接继承 RenderBox,然后混入 RenderObjectWithChildMixin 和 RenderProxyBoxMixin。RenderProxyBoxMixin 如其名,直接继承自 RenderObject,然后添加一个 child 属性。

RenderBox.hitTest

 由上面的 RenderView.hitTest 平滑进入 RenderBox.hitTest,看到 result 参数已经由空的 HitTestResult 对象被转化为空的 BoxHitTestResult 对象,Offset position 参数则保持不变。然后看到 RenderBox.hitTest 函数内部用到了 this 指针的 _size 属性,点开看到当前它的值是:Size(393.0, 852.0) 也就是当前 iPhone 15 Pro 的屏幕尺寸分辨率,目前已知的 Render Tree 中前面的一些节点的 _size 都是当前屏幕的尺寸。那么显然我们的入参 Offset position:Offset(194.7, 163.7) 肯定是在这个范围的,即:_size!.contains(position) 返回 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;
  }

 显然在第二个 if 处,会进入 hitTestChildren(result, position: position) 调用,由于 RenderSemanticsAnnotations、RenderProxyBox、RenderBox、RenderObjectWithChildMixin、RenderProxyBoxMixin 中,只有 RenderProxyBoxMixin 重写了 hitTestChilderen 函数中,所以接下来不出意外的调用到了 RenderProxyBoxMixin.hitTestChildren 中。

RenderProxyBoxMixin.hitTestChildren

 RenderProxyBoxMixin.hitTestChildren 函数超简单,就是继续往自己的子级中进行 hit testing,且没有任何 position 和 result 的变化。

  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
  
    
    return child?.hitTest(result, position: position) ?? false;
  }

 且看到当前的 this 指针依然是:RenderSemanticsAnnotations 类型。

 然后便是连续的 RenderProxyMixin.hitTestChildren 和 RenderBox.hitTest 的交替调用,它们分别位于 proxy_box.dart:130 和 box.dart:2762 的位置,所以我们只要看到堆栈末尾是它们两个就可知道此时调用到了它们两个函数。

截屏2024-10-01 14.04.00.png

 因为在这两个函数中是最基本的 hit test 过程,并没有牵涉到任何坐标位置的变换,仅仅是由父级 RenderBox 向子级 RenderBox 中调用 hitTest 函数的过程,所以下面我们重点放在图示中箭头指向的函数堆栈上。

 下面我们避开 RenderProxyBoxMixin.hitTestChildren 和 RenderBox.hitTest 函数,看一下其它的 RenderBox 子类是如何参与 hit testing 过程的,首先是 RenderTapRegionSurface。

RenderTapRegionSurface.hitTest

&emps;看到 RenderTapRegionSurface 对 hitTest 函数的重写,基本和 RenderBox.hitTest 相同,仅仅是多了 _cachedResults[entry] = result; 缓存过程。

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
  
    
    if (!size.contains(position)) {
      return false;
    }

    final bool hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);

    if (hitTarget) {
      final BoxHitTestEntry entry = BoxHitTestEntry(this, position);
      
      
      _cachedResults[entry] = result;
      
      result.add(entry);
    }

    return hitTarget;
  }

RenderCustomPaint.hitTestChildren

 RenderCustomPaint 也是一个直接继承自 RenderProxyBox 的子类。它重写了自己的 hitTestChildren 函数。

  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    
    
    
    if (_foregroundPainter != null && (_foregroundPainter!.hitTest(position) ?? false)) {
      return true;
    }
    
    return super.hitTestChildren(result, position: position);
  }

RenderProxyBoxWithHitTestBehavior.hitTest

 第一次遇到 RenderProxyBoxWithHitTestBehavior.hitTest 时,可看到当前的 this 指针指向一个 depth 是 10 的 RenderPointerListener 实例对象。

&emp;RenderPointerListener 直接继承自 RenderProxyBoxWithHitTestBehavior,而 RenderProxyBoxWithHitTestBehavior 直接继承自 RenderProxyBox,由于仅有 RenderProxyBoxWithHitTestBehavior 重写了 hitTest 函数,所以这里是调用到了 RenderProxyBoxWithHitTestBehavior.hitTest 这里。



class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { 






abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox { 

 RenderProxyBoxWithHitTestBehavior 仅是在自己的父类 RenderProxyBox 的基础上,添加了一个名为 behavior 的属性,以及重写了 hitTest 和 hitTestSelf 函数,来让此 behavior 直接参与 hit test 的过程。

 behavior 属性值的类型是 HitTestBehavior 枚举。HitTestBehavior 枚举有三个值,分别表示 RenderProxyBoxWithHitTestBehavior 及其子类在进行 hit test 时的不同处理方式:

  1. deferToChild:如果 target 委托给其子级,在边界内只有在 hti test 触碰到其子级之一时才会接收事件。
  2. opaque:不透明 target 可以被 hti test 击中,从而使它们在其范围内接收事件,并阻止位于其后的其他 target 也接收事件。
  3. translucent:半透明 target 既可以接收其边界内的事件,又可以让位于其后的 target 也能够接收事件。
  @override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    bool hitTarget = false;
    
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      
      
      
      if (hitTarget || behavior == HitTestBehavior.translucent) {
        result.add(BoxHitTestEntry(this, position));
      }
    }
    
    return hitTarget;
  }

 OK,进行到这里发现还是没有到坐标转换到函数堆栈,其实它们比较靠后,前面都是系统为我们在 Render Tree 中添加的辅助性的 Render 节点,且它们多为 RenderProxyBox 的子类,且以此可以明确到它们都是仅有一个子级的 Render 节点,直到后续我们遇到 ContainerRenderObjectMixin 时,才会看到多子级的情况,它们的 defaultHitTestChildren 函数中会循环对子级进行 hitTest。

 鉴于篇幅长度,本篇先到这里,我们下篇继续。下篇开始看以 RenderCustomMultiChildLayoutBox 为起点的多子级的 RenderBox 的 hit test 过程。

参考链接

参考链接:🔗

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21069,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?