一文深入了解Flutter事件机制

背景

Flutter作为一个跨平台的UI开发框架,有着自己独立的一套UI框架已经渲染引擎,那么它肯定有着自己一套独立的事件机制,用于分发管理各种各样的点击,双击,滑动等等事件,本文从Android平台层原生视图FlutterView出发,来探讨一下Flutter中的的事件分发机制。

Flutter事件从哪来

我们知道在不同的平台都存在着一个承载FlutterUI的平台层容器,我们这里就以Android端的FlutterView为入口,开始来了解下Android 平台下事件是如何分发到Flutter Framework层的。

onTouchEvent

首先我们来看FlutterView的`onTouchEvent:

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
  if (!isAttachedToFlutterEngine()) {
    return super.onTouchEvent(event);
  }
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    requestUnbufferedDispatch(event);
  }
  // 事件直接交给androidTouchProcessor处理了
  return androidTouchProcessor.onTouchEvent(event);
}

我们再来看androidTouchProcessor.onTouchEvent是何方神圣

public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) {
  int pointerCount = event.getPointerCount();

  // 对event进行转换和存储的对象
  ByteBuffer packet =
      ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
  packet.order(ByteOrder.LITTLE_ENDIAN);

  // 事件转换存储
  addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, transformMatrix, packet);
 
  // 将packet发送到flutter
  renderer.dispatchPointerDataPacket(packet, packet.position());
  
	// 事件消费完成
  return true;
}

我们看到这个方法的核心流程主要做了两件事:

  • 将平台层的MotionEvent包装,转换成Flutter能看懂的packet
  • 将packet发送到flutter

dispatchPointerDataPacket

我们继续看flutterRender.dispatchPointerDataPacket

public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) {
  flutterJNI.dispatchPointerDataPacket(buffer, position);
}

dispatchPointerDataPacket方法直接调用到了flutterJNI层的dispatchPointerDataPacket方法。

engine的分发流程我们这里略过。。。它最终通过engine派发到了dart侧的window.onPonterDataPacket方法。

华丽分割线…


onPonterDataPacket

我们先来看看window.onPonterDataPacket的初始化是在GestureBinding的mixin类中。之前分析runApp启动流程的时候讲过。

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    window.onPointerDataPacket = _handlePointerDataPacket;
  }
}

继续看window.onPointerDataPacket方法

set onPointerDataPacket(PointerDataPacketCallback? callback) {
  platformDispatcher.onPointerDataPacket = callback;
}

继续看platformDispatcher.onPointerDataPacket

set onPointerDataPacket(PointerDataPacketCallback? callback) {
  _onPointerDataPacket = callback;
  _onPointerDataPacketZone = Zone.current;
}

最终_handlePointerDataPacket回调方法赋值给了platformDispatcher的_onPointerDataPacket。而_onPointerDataPacket最终会在下面方法中调用

// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) {
  if (onPointerDataPacket != null) {
    _invoke1<PointerDataPacket>(
      onPointerDataPacket,
      _onPointerDataPacketZone,
      _unpackPointerDataPacket(packet),
    );
  }
}

从注解中看到,这里的方法最终与咱们在engine层中的调用链关联了起来,当然engine中关联的节点是在hooks.dart文件中

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
  PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}

_handlePointerDataPacket

好了到了这里,我们大概知道是是如何一步一步派发到window.onPointerDataPacket,接下来继续看window.onPointerDataPacket的实现方法_handlePointerDataPacket

void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();
}

这里会先将packet加入到_pendingPointerEvents队列中,然后调用_flushPointerEventQueue方法

void _flushPointerEventQueue() {
  assert(!locked);

  while (_pendingPointerEvents.isNotEmpty)
    handlePointerEvent(_pendingPointerEvents.removeFirst());
}

_flushPointerEventQueue方法会循环处理队列所有的PointerEvent。

handlePointerEvent

handlePointerEvent方法最终会调用到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();
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
    hitTestResult = _hitTests[event.pointer];
  }
  if (hitTestResult != null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
      // 第二步
    dispatchEvent(event, hitTestResult);
  }
}

_handlePointerEventImmediately

_handlePointerEventImmediately这个方法主要功能是两个:

  • 第一步,命中测试。先创建root hitTestResult,内部有一个List类型的_path,随后调用了rootView的hitTest方法

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

    会递归调用它每个child的命中测试hitTest方法,便会根据自身的_size是否包含pointer event positon来确定是否加入到_path中(也就是result中)。后面会详细介绍他的判断流程。

  • 第二步

    遍历通过命中测试的所有节点。调用dispatchEvent方法,该方法会遍历_path中的节点,并调用handleEvent方法:

    @override // from HitTestDispatcher
    void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
      for (final HitTestEntry entry in hitTestResult.path) {
         entry.target.handleEvent(event.transformed(entry.transform), entry);
      }
    }
    

​ 会遍历调用每个节点的handleEvent,所以每个组件只需要重写 handleEvent 方法就可以处理事件了。

命中测试

一个对象是否响应事件,取决于在对它进行命中测试过程中是否被添加到了HitTestResult列表,如果没有被添加进去,则后续的事件分发将不会分发给自己。下面我们一起来看看命中测试的整体流程:

当用户事件分发过来时,Flutter会从RenderBinding的hitTest()方法开始到根节点(RenderView)的hitTest()方法,再依次遍历整个renderObject tree的hitTest(),

先来看RenderBinding的hitTest方法

@override
void hitTest(HitTestResult result, Offset position) {
  // 从根节点开始进行命中测试
  renderView.hitTest(result, position: position);
  // 会调用 GestureBinding 中的 hitTest()方法,这里处理手势的一些问题。另外的文章介绍
  super.hitTest(result, position);
}

这里分为两步

  1. renderView 是 RenderView 对应的 RenderObject 对象, RenderObject 对象的 hitTest 方法主要功能是:从该节点出发,按照深度优先的顺序递归遍历子树(渲染树)上的每一个节点并对它们进行命中测试。这个过程称为“渲染树命中测试”。

注意,为了表述方便,“渲染树命中测试”,也可以表述为组件树或节点树命中测试,只是我们需要知道,命中测试的逻辑都在 RenderObject 中,而并非在 Widget或 Element 中。

  1. 渲染树命中测试完毕后,会调用 GestureBinding 的 hitTest 方法,该方法主要用于处理手势。

渲染树的命中测试流程

我们先来看RenderView的hitTest()源码:

 发起命中测试,position 为事件触发的坐标(如果有的话)。  
bool hitTest(HitTestResult result, { required Offset position }) {
    if (child != null)
      child!.hitTest(BoxHitTestResult.wrap(result), position: position);
    //根节点会始终被添加到HitTestResult列表中
    result.add(HitTestEntry(this));
    return true;
  }

因为 RenderView 只有一个孩子,所以直接调用child.hitTest 即可。如果一个渲染对象有多个子节点,则命中测试逻辑为:如果任意一个子节点通过了命中测试或者当前节点“强行声明”自己通过了命中测试,则当前节点会通过命中测试。我们以RenderBox为例,看看它的hitTest()实现:

bool hitTest(HitTestResult result, { @required Offset position }) {
  ...  
  if (_size.contains(position)) { // 判断事件的触发位置是否位于组件范围内
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

上面代码中:

  • hitTestChildren() 功能是判断是否有子节点通过了命中测试,如果有,则会将子组件添加到 HitTestResult 中同时返回 true;如果没有则直接返回false。该方法中会递归调用子组件的 hitTest 方法。
  • hitTestSelf() 决定自身是否通过命中测试,如果节点需要确保自身一定能响应事件可以重写此函数并返回true ,相当于“强行声明”自己通过了命中测试。

需要注意,节点通过命中测试的标志是它被添加到 HitTestResult 列表中,而不是它 hitTest 的返回值,虽然大所数情况下节点通过命中测试就会返回 true,但是由于开发者在自定义组件时是可以重写 hitTest 的,所以有可能会在在通过命中测试时返回 false,或者未通过命中测试时返回 true,当然这样做并不好,我们在自定义组件时应该尽可能避免,但是在有些需要自定义命中测试流程的场景下可能就需要打破这种默契。

所以整体逻辑就是:

  1. 先判断事件的触发位置是否位于组件范围内,如果不是则不会通过命中测试,此时 hitTest 返回 false,如果是则到第二步。
  2. 会先调用 hitTestChildren() 判断是否有子节点通过命中测试,如果是,则将当前节点添加到 HitTestResult 列表,此时 hitTest 返回 true。即只要有子节点通过了命中测试,那么它的父节点(当前节点)也会通过命中测试。
  3. 如果没有子节点通过命中测试,则会取 hitTestSelf 方法的返回值,如果返回值为 true,则当前节点通过命中测试,反之则否。

如果当前节点有子节点通过了命中测试或者当前节点自己通过了命中测试,则将当前节点添加到 HitTestResult 中。又因为 hitTestChildren()中会递归调用子组件的 hitTest 方法,所以组件树的命中测试顺序深度优先的,即如果通过命中测试,子组件会比父组件会先被加入HitTestResult 中

我们看看这两个方法默认实现如下:

@protected
bool hitTestChildren(HitTestResult result, { Offset position }) => false;

@protected
bool hitTestSelf(Offset position) => false;

如果组件包含多个子组件,就必须重写 hitTestChildren() 方法,该方法中应该调用每一个子组件的 hitTest 方法,比如我们看看 RenderBoxContainerDefaultsMixin 中的实现:

// 子类的 hitTestChildren() 中会直接调用此方法
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
   // 遍历所有子组件(子节点从后向前遍历)
  ChildType? child = lastChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    // isHit 为当前子节点调用hitTest() 的返回值
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      //调用子组件的 hitTest方法,
      hitTest: (BoxHitTestResult result, Offset? transformed) {
        return child!.hitTest(result, position: transformed!);
      },
    );
    // 一旦有一个子节点的 hitTest() 方法返回 true,则终止遍历,直接返回true
    if (isHit) return true;
    child = childParentData.previousSibling;
  }
  return false;
}

  bool addWithPaintOffset({
    required Offset? offset,
    required Offset position,
    required BoxHitTest hitTest,
  }) {
    ...// 省略无关代码
    final bool isHit = hitTest(this, transformedPosition);
    return isHit; // 返回 hitTest 的执行结果
  }

我们可以看到上面代码的主要逻辑是遍历调用子组件的 hitTest() 方法,同时提供了一种中断机制:即遍历过程中只要有子节点的 hitTest() 返回了 true 时:

  1. 会终止子节点遍历,这意味着该子节点前面的兄弟节点将没有机会通过命中测试。注意,兄弟节点的遍历倒序的。
  2. 父节点也会通过命中测试。因为子节点 hitTest() 返回了 true 导父节点 hitTestChildren 也会返回 true,最终会导致 父节点的 hitTest 返回 true,父节点被添加到 HitTestResult 中。

当子节点的 hitTest() 返回了 false 时,继续遍历该子节点前面的兄弟节点,对它们进行命中测试,如果所有子节点都返回 false 时,则父节点会调用自身的 hitTestSelf 方法,如果该方法也返回 false,则父节点就会被认为没有通过命中测试。

如果不重写 hitTestChildren,则默认直接返回 false,这也就意味着后代节点将无法参与命中测试,相当于事件被拦截了,这也正是 IgnorePointer 和 AbsorbPointer 可以拦截事件下发的原理。

如果 hitTestSelf 返回 true,则无论子节点中是否有通过命中测试的节点,当前节点自身都会被添加到 HitTestResult 中。而 IgnorePointer 和 AbsorbPointer 的区别就是,前者的 hitTestSelf 返回了 false,而后者返回了 true。

命中测试完成后,所有通过命中测试的节点都被添加到了 HitTestResult 中。

Flutter事件处理流程

Demo

简单的实现事件监听(监听原始down事件),方便理解后面Flutter的事件处理

  1. 自定义Widget,继承SingleChildRenderObjectWidget类

    class PointerDownListener extends SingleChildRenderObjectWidget {
      PointerDownListener({Key? key, this.onPointerDown, Widget? child})
          : super(key: key, child: child);
    
      final PointerDownEventListener? onPointerDown;
    
      @override
      RenderObject createRenderObject(BuildContext context) =>
          RenderPointerDownListener()..onPointerDown = onPointerDown;
    
      @override
      void updateRenderObject(
          BuildContext context, RenderPointerDownListener renderObject) {
        renderObject.onPointerDown = onPointerDown;
      }
    }
    
  2. 自定义RenderObject,继承自RenderProxyBox

    class RenderPointerDownListener extends RenderProxyBox {
      PointerDownEventListener? onPointerDown;
    
     
      @override
      bool hitTestSelf(Offset position) => true; //这里直接返回为true,始终通过命中测试
    
      @override
      void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
        //事件分发时处理事件
        if (event is PointerDownEvent) onPointerDown?.call(event);
      }
    }
    
  3. 使用

    class PointerDownListenerRoute extends StatelessWidget {
      const PointerDownListenerRoute({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return PointerDownListener(
          child: Text('Click me'),
          onPointerDown: (e) => print('down'),
        );
      }
    }
    

    点击文本后控制台就会打印出 ‘down’

Listener理解

Listener的实现和上面的PointerDownListener的实现原理差不多。源码也很好理解,这里就不重复介绍。

Listener简单用法:

class _MyHomePageState extends State<MyHomePageWidget> {
  PointerEvent _event;

  @override
  Widget build(BuildContext context) {
    // 监听通知
    return Listener(
      child: Container(
        color: Colors.white,
        child: Center(
            child: Container(
          width: 200,
          height: 200,
          color: Colors.yellow,
          child: Center(
            child: Text(
              "down location:${_event?.localPosition}",
              style: TextStyle(fontSize: 15),
            ),
          ),
        )),
      ),
      onPointerDown: (PointerDownEvent event) => setState(() {
        _event = event;
      }),
    );
  }
}

结果如图,无论点击屏幕那个位置,都能成功刷新坐标位置

image-20211111204920998

Listener注意事项

我们在看Listener源码的时候,除了各种各样的EventListener好理解外,还有一个HitTestBehavior类型的behavior对象。它的渲染对象 RenderPointerListener 继承了 RenderProxyBoxWithHitTestBehavior 类

abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
  /// [behavior] 的默认值为 [HitTestBehavior.deferToChild].
  RenderProxyBoxWithHitTestBehavior({
    this.behavior = HitTestBehavior.deferToChild,
    RenderBox? child,
  }) : super(child);

  /// How to behave during hit testing.
  HitTestBehavior behavior;

  @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;
  }

  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(EnumProperty<HitTestBehavior>('behavior', behavior, defaultValue: null));
  }
}

我们看到 behavior 在 hitTest 和 hitTestSelf 中会使用,它的取值会影响 Listener 的命中测试结果。我们再来看看 behavior 都有哪些取值:

// 在命中测试过程中 Listener 组件如何表现。
enum HitTestBehavior {
  /// 组件是否通过命中测试取决于子组件是否通过命中测试
  deferToChild,

  /// 组件必然会通过命中测试,同时其 hitTest 返回值始终为 true
  opaque,

  /// 组件必然会通过命中测试,但其 hitTest 返回值可能为 true 也可能为 false
  translucent,
}

它有三个取值,我们结合 hitTest 实现来分析一下不同取值的作用:

  1. behavior 为 deferToChild 时,hitTestSelf 返回 false,当前组件是否能通过命中测试完全取决于 hitTestChildren 的返回值。也就是说只要有一个子节点通过命中测试,则当前组件便会通过命中测试。
  2. behavior 为 opaque 时,hitTestSelf 返回 true,hitTarget 值始终为 true,当前组件通过命中测试。
  3. behavior 为 translucent 时,hitTestSelf 返回 false,hitTarget 值此时取决于 hitTestChildren 的返回值,但是无论 hitTarget 值是什么,当前节点都会被添加到 HitTestResult 中。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>