Flutter 事件处理机制

学习参考资料Flutter实战

前置介绍

在 Flutter 中有两种布局模型,基于 RenderBox 的盒模型布局,基于 Sliver ( RenderSliver ) 按需加载列表布局,它们有一个共同特点就是都是继承自 RenderObject,其次都有定义 hitTest、hitTestChildren、hitTestSelf、handleEvent,这些方法都是 Flutter 事件处理关键的方法,但是RenderSliver 的命中测试逻辑是从 RenderViewport 开始的,RenderViewport 它是 RenderBox 子类,而 RenderBox 又继承自 RenderObject ,所以这里接下来的分析都是基于 RenderBox 模型来分析,首先通过一个入口 GestureBinding 类来分析 Flutter 的事件机制

GestureBinding 简介

  • 初始化时机:在 runApp 方法中初始化了 WidgetsFlutterBinding,而它 mix-in 了 RendererBinding 和 GestureBinding 类,从继承关系的角度来说:WidgetsFlutterBinding 继承 RendererBinding 继承 GestureBinding
  • 作用: GestureBinding 中包含 Flutter 事件的命中测试、事件分发处理、事件命中结果清理的逻辑,同时这也是 Flutter 事件的处理流程,关键方法是它的 _handlePointerEventImmediately

GestureBinding _handlePointerEventImmediately 方法源码

 void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      // 创建命中测试结果的集合
      hitTestResult = HitTestResult();
      // 执行命中测试,优先遍历 child 也就是深度遍历
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $hitTestResult');
        return true;
      }());
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    // 如果是 up cancel 事件就从命中测试结果中移除
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down) {
      hitTestResult = _hitTests[event.pointer];
    }
    assert(() {
      if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
        debugPrint('$event');
      return true;
    }());
    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      // 遍历分发命中测试的结果
      dispatchEvent(event, hitTestResult);
    }
  }
复制代码
总结:
  • 首先它有三个事件 PointerDownEvent、PointerSignalEvent、PointerHoverEvent 都会触发命中测试的逻辑
  • 这里关注 PointerDownEvent 事件即可,那么 Flutter 事件处理的流程就是
    • HitTestResult:首先会创建命中测试结果的集合
    • hitTest():执行命中测试,内部逻辑是优先遍历 child 也就是优先深度遍历
    • dispatchEvent():遍历命中测试结果集合(没有打断的事件分发逻辑),并调用对应命中测试目标的 handleEvent() 方法,那么因为上面命中测试是优先深度执行的,而遍历命中测试结果是正序遍历,所以子组件会比父组件优先响应事件,源码比较简单,下面就不在分析了,知道作用就行了
    • 如果是 up 或 cancel 事件就从命中测试结果中移除

hitTest 基础的执行流程(基于 RenderBox)

  1. RenderBinding hitTest
  @override
  void hitTest(HitTestResult result, Offset position) {
    assert(renderView != null);
    assert(result != null);
    assert(position != null);
    // renderView 相当于 Android 中的根 view 
    renderView.hitTest(result, position: position);
    // 调用 GestureBinding hitTest
    super.hitTest(result, position);
  }
复制代码
  1. RenderView hitTest
bool hitTest(HitTestResult result, { required Offset position }) {
    if (child != null)
    // 这里 child 是一个 RenderBox 类型,RenderBox 又继承自 RenderObject 
    // hitTest 是在 RenderBox 中定义的 
      child!.hitTest(BoxHitTestResult.wrap(result), position: position);
      // 将 RenderView 添加到 命中测试结果的结合中去
    result.add(HitTestEntry(this));
    return true;
}
复制代码
  1. RenderBox hitTest
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    // 省略一些代码 ...... 
    // 判断 是否在点击区域内
    if (_size!.contains(position)) {
        // 这里两个判断 
        // 1. 首先判断 RenderBox hitTestChildren 也就是先去执行子组件的命中测试逻辑
        // 2. 如果没有子组件命中,那么再判断自身是否命中 RenderBox hitTestSelf
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        // 如果子组件或者自身有一个命中那么自己也会被添加到命中测试结果集合中去
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
  }
复制代码
  1. 最终执行 GestureBinding hitTest
 @override // from HitTestable
  void hitTest(HitTestResult result, Offset position) {
    // 会把 GestureBinding 添加到命中测试的结果集合中去,用于之后去处理手势事件的逻辑
    result.add(HitTestEntry(this));
  }
复制代码
总结:
  1. 从上面的代码可以再次看出 hitTest 执行流程是优先深度遍历的,也就是先去执行子组件的 hitTest,再执行父组件的 hitTest
  2. RenderView 继承自 RenderBox 继承自 RenderObject,这里重点介绍下 RenderBox 中的三个方法:
    • hitTest:执行命中测试的逻辑
    • hitTestChildren:执行子组件的命中测试,如果不重写默认返回 false
    • hitTestSelf:自身是否命中测试,如果没有重写默认返回 false

这三个方法是 Flutter 事件处理的关键,同时也可以看出事件的处理逻辑是在 RenderBox 或者说 RenderObject 中的,使用 RenderObject 来理解的话,可以与 Flutter 三颗树的概念相结合。且可以看到从 RenderView 根 View 开始 Flutter 命中测试的流程为:自身 hitTest 被调用,并在其中先判断执行 hitTestChildren 如果子组件没有命中测试,才去判断执行 hitTestSelf,最后有一个通过了那么当前组件就会被添加到命中测试结果的集合中去,可以通过下面这张图来总结一下这三个方法的大概执行流程:

hitTest执行流程.png

  1. 手势是最后被添加到命中测试结果集合中的,那么在事件分发流程中也会去调用其 handleEvent() 方法,也就是意味着,手势的处理是在 Flutter 事件分发的流程中处理的

接下来通过 Stack 角度来进一步分析,因为 Stack 的 RenderStack 没有重写 RenderBox 的 hitTest 和 hitTestSelf 方法,且这两个方法上面已经分析过了,那么直接看它的 hitTestChildren 方法源码即可。

RenderStack hitTestChildren

因为上面的源码分析总结提到:事件的处理逻辑是在 RenderBox 或者说 RenderObject 中的,所以这里要看 Stack 创建的 RenderObject 类 - RenderStack

  1. Stack 中 createRenderObject 方法创建了 RenderStack,RenderStack 继承自 RenderBox (RenderObject 子类)
  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    // 调用了 RenderBox 的 defaultHitTestChildren
    return defaultHitTestChildren(result, position: position);
  }
复制代码
  1. RenderBox 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);
          // 执行了 child 的 hitTest
          return child!.hitTest(result, position: transformed!);
        },
      );
      // 如果有一个 child 命中那么就结束这个命中测试的循环
      if (isHit)
        return true;
       // 倒序遍历 取 lastChild 的上一个兄弟节点
      child = childParentData.previousSibling;
    }
    return false;
  }
  
复制代码
总结:
  1. 对于子组件执行 hitTest 的遍历是倒序遍历,为什么呢?这里以 Stack 布局为例,因为后添加的子组件是可能会覆盖前面的兄弟组件,那么对于用户点击来说肯定是最上面组件来响应事件的,因为下面的兄弟节点都被覆盖了
  2. 对于子组件来说,一旦有一个返回了 true,那么其它的子组件就不会在执行 hitTest了,也就不会被添加到命中测试的结果中去了,后续也不会去处理事件了,但是如果想要让多个子组件都响应事件,也可以让子组件的 hitTest 返回 false,同时在子组件中的 hitTest 方法中将子组件添加到命中测试的结果结合中
注意:组件是否可以处理事件的标志是有没有被添加到命中测试的结果集合中

通过代码来实际分析 Flutter 的事件机制流程

Stack(
      alignment: AlignmentDirectional.center,
      children: [
        // Flutter 中 使用 Listener 来监听原始触摸事件
        Listener(
          child: GestureDetector(
            child: const Text(
              "点击我",
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 18
              ),
            ),
            onTap: (){
              print("=GestureDetector=onTap=1==");
            },
          ),
          onPointerDown: (event) {
            print("=Listener=onPointerDown=1==");
          },
          onPointerUp: (event){
            print("=Listener=onPointerUp=1==");
          },
        ),
      ],
    )
复制代码

上面这段代码当点击 “点击我” 的 Text 时的执行结果为:

I/flutter (17505): =Listener=onPointerDown=1==
I/flutter (17505): =Listener=onPointerUp=1==
I/flutter (17505): =GestureDetector=onTap=1==

通过上面的源码分析总结来画出它的流程图:

代码的角度来看hitTest流程.png

代码的角度来看dispatchEvent过程.png

关于手势的处理机制可以参考这篇文章:Flutter 手势事件处理机制

事件的拦截:

Flutter 提供了 AbsorbPointer 和 IgnorePointer 它们两都可以拦截子组件的事件,但是又有区别,主要在于 hitTest 方法的处理逻辑上,下面看它们的源码:

AbsorbPointer - RenderAbsorbPointer(Renderobject子类) - hitTest 源码:

	@override
   bool hitTest(BoxHitTestResult result, { required Offset position }) {
     return absorbing
         ? size.contains(position)
       		// RenderBox hitTest 方法
         : super.hitTest(result, position: position);
   }
复制代码

IgnorePointer - RenderIgnorePointer (Renderobject子类)- hitTest 源码:

  @override
   bool hitTest(BoxHitTestResult result, { required Offset position }) {
     return !ignoring && 
       // RenderBox hitTest 方法
       super.hitTest(result, position: position);
   }
复制代码
总结:
  1. AbsorbPointer 如果 absorbing 为 true,那么 super.hitTest 方法(RenderBox hitTest)不会执行,也就不会执行 hitTestChildren 方法,那么子组件就不会有响应事件的机会,同时 AbsorbPointer 的 hitTest 方法会返回 true ,以 RenderStack hitTestChildren 方法为例,那么它的兄弟组件也不会响应事件了
  2. IgnorePointer 如果 ignoring 为 true 那么 因为 && 的关系,后面的 super.hitTest 方法(RenderBox hitTest)也不会执行,所以子组件不会有响应事件的机会,同时 IgnorePointer hitTest 方法会返回 false ,以 RenderStack hitTestChildren 方法为例它不会影响兄弟组件的事件响应

猜你喜欢

转载自juejin.im/post/7074618358758375454