Flutter事件分发源码剖析

·  阅读 35

这篇文章主要为大家详细介绍了Flutter事件分发源码剖析,感兴趣的小伙伴们可以参考一下。

概述

不管是原生Android、iOS还是JavaScript,只要是涉及手势交互都会有事件的分发处理。和原生Android、iOS的事件分发的步骤和原理一样,Flutter的事件分发总体也由手势触发、拦截和响应等几个部分构成。Flutter所有事件源头是 hooks.dart文件的_dispatchPointerDataPacket函数,通过拦截屏幕的点击、滑动等各种事件,进而分发给原生代码进行响应。

如果你看过了解原生Android、iOS的事件分发机制,那么Flutter的事件分发,其实是在Android和iOS上加了壳,即Flutter的事件分发是在原生Android、iOS的的事件分发上进行包装的(Android - C - Dart,iOS- C -Dart)。其中,C是Flutter的底层engine,负责Flutter上层和原生Android、iOS系统的交互。

事件分发到Dart的入口类是GestureBinding类,此类位于gestures/binding.dart文件中,与手势识别相关的都位于gestures包中,如下图所示。

1.converter.dart将物理坐标_dispatchPointerDataPacket收到的物理数据PointerDataPacket转换成PointerEvent, 类似于安卓在ViewRootImpl.java将InputEventReceiver收到的InputEvent转换为MotionEvent。

2.recognizer.dart的GestureRecognizer是所有手势识别的基类。 3.rendering/binding.dart的RendererBinding类关联了render树和Flutter引擎,等价于安卓的Surface。 4.view.dart的RenderView是render树的根节点,等价于安卓的DecorView

Flutter的事件分发基类是GestureBinding,打开GestureBinding类,它的成员函数包括dispatchEvent、handleEvent和hitTes等,主要是从事件队列里按照先入先出方式处理PointerEvent,源码如下。

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

其中,WidgetsFlutterBinding.ensureInitialized()函数的作用就是初始化各个binging。

Flutter 事件分发

和Android、iOS类似,Flutter的事件分发的入口在runApp函数,相关的代码如下

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}
 
void attachRootWidget(Widget rootWidget) {
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget
  ).attachToRenderTree(buildOwner, renderViewElement);
}
复制代码

WidgetsFlutterBinding.ensureInitialized()函数的作用是初始化各个binging。事实上,Flutter 中的 WidgetsFlutterBinding的 Binding可以分为GestureBinding、ServicesBinding、SchedulerBinding、PaintingBinding、SemanticsBinding、RendererBinding、WidgetsBinding 等 7 种 Binding,它们都有自己在功能上的划分。其中,GestureBinding就是处理事件分发的,attachRootWidget就是设置根节点, 可以看到真正的根节点是renderview, 也是Flutter事件分发的起点。

下面我们来重点看一下GestureBinding类。 ###GestureBinding 和Android事件处理的流程一样,首先,系统会拦截用户的事件,然后在使用GestureBinding的_handlePointerEvent进行事件命中处理。原生事件到达Dart层之后调用的第一个方法是_handlePointerDataPacket,它的源码如下。

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

_handlePointerDataPacket方法有一个PointerEventConverter类,作用是将原生传来的手势数据全部转化为Dart对应的对象保存数据,然后保存到集合中进行储存。接下来来我们看一下_flushPointerEventQueue方法,源码如下。

void _flushPointerEventQueue() {
    assert(!locked);
    while (_pendingPointerEvents.isNotEmpty)
      _handlePointerEvent(_pendingPointerEvents.removeFirst());
  }
复制代码

_flushPointerEventQueue方法的作用就是循环处理每个手指的的事件,并进行处理,源码如下。

void _handlePointerEvent(PointerEvent event) {
    assert(!locked);
    HitTestResult hitTestResult;
    //如果是手指按下的话
    if (event is PointerDownEvent || event is PointerSignalEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      //得到碰撞的控件组
      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) {
      hitTestResult = _hitTests.remove(event.pointer);
    }
    //缓存点击的事件,接下来发生滑动的时候直接复用原来的碰撞控件组
    else if (event.down) {
      // Because events that occur with the pointer down (like
      // PointerMoveEvents) should be dispatched to the same place that their
      // initial PointerDownEvent was, we want to re-use the path we found when
      // the pointer went down, rather than do hit detection each time we get
      // such an event.
      hitTestResult = _hitTests[event.pointer];
    }
    assert(() {
      if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
        debugPrint('$event');
      return true;
    }());
    if (hitTestResult != null ||
        event is PointerHoverEvent ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      dispatchEvent(event, hitTestResult);
    }
  }
复制代码

这个方法的主要目的就是得到HitTestResult,就是根据按下的坐标位置找出view树中哪些控件在点击的范围内,手指在移动和抬起的时候都复用当前的事件,区别在于不同的手指有不同的索引值。接下来,看一下用户的触摸行为,hitTest首先会进入RendererBinding处理,打开RendererBinding类的hitTest方法,如下所示。

RenderView get renderView => _pipelineOwner.rootNode as RenderView;

void hitTest(HitTestResult result, Offset position) {
    assert(renderView != null);
    renderView.hitTest(result, position: position);
    super.hitTest(result, position);
  }
复制代码

其中,RenderView可以理解为Flutter 视图树的根View,在Flutter中也叫做Widget ,一个Widget 对应一个Element 。在Flutter中,渲染会三棵树,即Widget 树、Element 树和RenderObject 树。我们进行页面布局分析时,就可以看到它们,如下所示。

关于Widget 树、Element 树和RenderObject 树,可以查看[Flutter渲染之Widget、Element 和 RenderObject]的介绍。

然后,我们打开renderView.hitTest方法,对应的代码如下所示。

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

可以看到,根视图是先从子view开始放进集合,放完子view再放自己,这和前端JS点击事件冒泡的原理是一样的。并且,只有满足条件子视图才会放到 入RenderBox 的这个方法中。

 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;
  }
复制代码

接下来,看一下Stack小部件hitTestChildren的实现,源码如下。

  @override
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
    return defaultHitTestChildren(result, position: position);
  }

bool defaultHitTestChildren(BoxHitTestResult result, { 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;
      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;
  }
复制代码

使用Listener嵌套的子组件默认情况下是命中的,很多子部件例如Text、Image等,它们的hitTestSelf返回True,假如我们为Text嵌套了Listener,那么事件分发的时候设计的代码如下所示。

void handleEvent(PointerEvent event, HitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (onPointerDown != null && event is PointerDownEvent)
      return onPointerDown(event);
    if (onPointerMove != null && event is PointerMoveEvent)
      return onPointerMove(event);
    if (onPointerUp != null && event is PointerUpEvent)
      return onPointerUp(event);
    if (onPointerCancel != null && event is PointerCancelEvent)
      return onPointerCancel(event);
    if (onPointerSignal != null && event is PointerSignalEvent)
      return onPointerSignal(event);
  }
复制代码

如果手指按下的时候GestureRecognizer的handleEvent方法没有决策出到底哪个控件会成为事件的处理者,那么会执行 gestureArena.close()方法,如下所示。

void close(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isOpen = false;
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
    _tryToResolveArena(pointer, state);
  }
复制代码

如果未决策出哪个控件处理事件的时候,state.isOpen此时被标记为false,也即是关闭手势的处理。

void _tryToResolveArena(int pointer, _GestureArena state) {
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    if (state.members.length == 1) {
      scheduleMicrotask(() => _resolveByDefault(pointer, state));
    } else if 
 
(state.members.isEmpty) {
      _arenas.remove(pointer);
      assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
    } 
else if (state.eagerWinner != null) {
      assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
      _resolveInFavorOf(pointer, state, state.eagerWinner);
    }
  }
复制代码

如果手势竞争中,有竞争胜出者,则由胜出者执行事件处理,如下所示。

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
    _arenas.remove(pointer);
    //其他的命中全部拒绝
    for (GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
  }
复制代码

如果事件处理中没有具体的事件处理对象,将会默认采用最底层的的叶子节点控件作为事件处理者,也就是说最内层的那个控件将消耗事件。也就是说,如果使用GestureRecognizer来识别手势事件时,最终事件会被最内层的GestureRecognizer消耗,这和Android单个控件消耗事件差不多,所以嵌套滚动总是先滚动内层,先被内层消耗,然后再执行外层。

以上就是本文的全部内容,希望对大家有所帮助

原文地址:segmentfault.com/a/119000002…

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改