Flutter底层学习:事件

jkouu 202 0

1.事件的传递和处理

同Android一样,Flutter主要处理的事件也是DOWN、MOVE、UP、CANCEL等事件组成的事件流。

1.1.确定控件

binding.dart中的GestureBinding是Flutter事件处理链的源头,我们就以它为线索来学习Flutter的事件处理。

void _handlePointerEvent(PointerEvent event) {
  HitTestResult hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent) {
    //Down事件是事件流的开始
    hitTestResult = HitTestResult();
    //通过事件的位置确定受影响的控件
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
        //_hitTests是一个Map
       //将这些控件暂存在一个Map中,以备后面的复用
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    //UP和CANCEL事件标志一个事件流的结束,受该事件六影响的控件以后不会再有
    //被复用的情况,可以从Map中删除
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
    //不是DOWN、UP、CANCEL并且event的状态还处于down的情况,那就是MOVE事件了。
    //可以看到,MOVE事件是直接复用的DOWN事件的控件  
    hitTestResult = _hitTests[event.pointer];
  }
  if (hitTestResult != null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    //受影响的控件都已经确定了,接下来可以进行事件分发了      
    dispatchEvent(event, hitTestResult);
  }
}

上面是GestureBinding处理事件流的函数,也可以说是事件传递的一个预处理。通过这个函数,Flutter可以通过事件流的位置来确定哪些控件收到了影响。Flutter将受到影响的控件按照Widget树的层次顺序排序,然后再对它们进行事件分发。

要理解这个函数的实现,我们需要先知道几个函数中用到的类。

abstract class PointerEvent with Diagnosticable {
  final int pointer;
  final Offset position;
  final Offset localPosition;
}

首先是PointerEvent类。显然,函数中用到的PointerDownEvent、PointerUpEvent、PointerCancelEvent这是事件类都是这个抽象类的实现。上面两个属性是函数中用到的两个属性,pointer是一个事件流的唯一标识。注意,是事件流的唯一标识。也就是说,同一个事件流里,DOWN、UP、CANCEL、MOVE的pointer都是相同的。这很好理解,但出现多点触碰的情况时,会同时产生好几个事件流,这时就需要通过pointer来对它们分别进行管理。

position用来标识事件发生的位置。除了position,PointerEvent类还有一个localPosition属性。因为Flutter是一个跨平台框架,它在逻辑上处理事件的发生位置时应该是与设备无关的。也就是说,Flutter框架一开始接收的position数据只是一个逻辑上的值,Flutter通过这个值来确定事件能影响到哪些控件,但是在反映到设备上时,使用的是localPosition的数值。其实它们就是一个映射关系。

abstract class HitTestTarget {
  factory HitTestTarget._() => null;
  void handleEvent(PointerEvent event, HitTestEntry entry);
}

class HitTestEntry {
  HitTestEntry(this.target);
  final HitTestTarget target;
}

class HitTestResult {
  Iterable<HitTestEntry> get path => _path;
  final List<HitTestEntry> _path;
  void add(HitTestEntry entry) {
    entry._transform = _transforms.isEmpty ? null : _transforms.last;
    _path.add(entry);
  }
}

然后是HitTestResult类和它所用到的两个功能类。上面的源码是HitTestResult类源码中的节选。通过名字可以大致猜到,一个HitTestResult保存了一个事件流所影响的所有控件。控件以HitTestEntry的形式存储在_path中。对HitTestEntry再拆分,得到的是一个HitTestTarget接口。我们看到这个接口是有一个分发事件的handleEvent方法的,据此我们猜测,应该所有的控件都实现了这个接口,通过这个接口可以来检测哪些控件受到了事件流的影响。

看到这儿我们发现,整个函数中并没有真正把事件传递链放进_path里。我们注意到,GestureBinding是一个mixin,那么一定有其它类来重写了它的一些方法。

通过重写查找,我们找到了一个继承了GestureBinding类的类:RendererBinding。

abstract RendererBinding{
  RenderView renderView;  
  @override
  void hitTest(HitTestResult result, Offset position) {
    renderView.hitTest(result, position: position);
    super.hitTest(result, position);
  }
}

可惜的是,RendererBinding是一个抽象类,我们还要进一步去看它拥有的RenderView。

class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>{
  RenderBox child;
  bool hitTest(HitTestResult result, { Offset position }) {
    if (child != null)
      child.hitTest(BoxHitTestResult.wrap(result), position: position);
    result.add(HitTestEntry(this));
    return true;
  }
}

RenderView继承自RenderObject,而RenderObject又实现了HitTestTarget,所以其实RenderView是间接实现了HitTestTarget接口的。我们看到RenderView也有一个并非重写的的hitTest方法,大致逻辑是先对自己的child进行检测,然后再将自己加入到受影响的控件集合中。

RenderView的child是一个RenderBox类型的实例,RenderBox也继承自RenderObject,所以它也间接实现了HitTestTarget接口。

abstract RenderBox extends RenderObject{
  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是一个抽象类,所有的Widget都继承自它,自然所有的Widget都继承了HitTestTarget接口,这也可以证明我们之前的猜测是正确的。从RenderBox自己拥有的hitTest方法来看,所有Widget受某一个事件流的影响有两种情况:它的child正在监听该事件或者它自己正在监听该事件。

简单总结一下这部分的结论:Flutter的事件流从DOWN事件开始,经过若干个MOVE事件,以UP事件或者CANCEL事件结束。在处理DOWN事件时,所有Widget按照自己继承的RenderBox类的hitTest方法依次判断自己是否受事件影响,最终形成了以目标节点-->父节点-->根节点(也就是RenderView)-->GestureBinding的顺序构成的事件处理链,UP、MOVE、CANCEL事件都在复用DOWN事件所产生的事件处理链。

1.2.事件分发

·@override 
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
  if (hitTestResult == null) {
    try {
      pointerRouter.route(event);
    } catch (exception, stack) {...}
    return;
  }
  for (final HitTestEntry entry in hitTestResult.path) {
    try {
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {...}
  }
}

GestureDetector的dispatchEvent函数实现了事件的分发。在_handlePointerEvent函数中,我们得到了每一个事件的处理链,dispatchEvent函数其实就是按照处理链的先后顺序来让Widget依次对事件进行响应。这一点与Android有所不同,Android的事件传递机制中,如果子控件处理了事件,那么父控件是直接返回的;在Flutter中,子控件是否处理事件,都不影响父控件是否处理事件。

2.GestureDetector的实现

这部分的内容非常长且枯燥。如果了解源码的实现,那么建议一定要连着看下去,不然很容易思路就乱了。如果只是想大致了解一下GestureDetector的实现过程,那么可以直接跳到文章最后的总结。

2.1.手势的判断

前面我们说了Flutter的事件处理链和事件分发,这里我们说一下Flutter的事件监听。

GestureDetector是Flutter为我们封装好的事件监听的功能类,可以识别大多数的手势,我们就以它为线索来学习Flutter的事件监听。

@override
Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}

上面是GestureDetector的build函数,中间有一大段生成gestures的内容,主要是定义各种手势的,我们先不看。函数的最后是根据前面定义的手势和行为生成创建RawFestureDetector实例,也就是说GestureDetector的功能主要是由RawGestureDetector完成的。

@override
Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if (!widget.excludeFromSemantics)
    result = _GestureSemantics(
      child: result,
      assignSemantics: _updateSemanticsForRenderObject,
    );
  return result;
}

RawGestureDetector是一个StatefulWidget,上面是它关联的State类的build函数。可以看到,它又是通过Listener来实现功能的。

class Listener extends StatelessWidget {

  /// Called when a pointer comes into contact with the screen (for touch
  /// pointers), or has its button pressed (for mouse pointers) at this widget's
  /// location.
  final PointerDownEventListener onPointerDown;

  /// Called when a pointer that triggered an [onPointerDown] changes position.
  final PointerMoveEventListener onPointerMove;

  /// Called when a pointer enters the region for this widget.
  ///
  /// This is only fired for pointers which report their location when not down
  /// (e.g. mouse pointers, but not most touch pointers).
  ///
  /// If this is a mouse pointer, this will fire when the mouse pointer enters
  /// the region defined by this widget, or when the widget appears under the
  /// pointer.
  final PointerEnterEventListener onPointerEnter;

  /// Called when a pointer that has not triggered an [onPointerDown] changes
  /// position.
  ///
  /// This is only fired for pointers which report their location when not down
  /// (e.g. mouse pointers, but not most touch pointers).
  final PointerHoverEventListener onPointerHover;

  /// Called when a pointer leaves the region for this widget.
  ///
  /// This is only fired for pointers which report their location when not down
  /// (e.g. mouse pointers, but not most touch pointers).
  ///
  /// If this is a mouse pointer, this will fire when the mouse pointer leaves
  /// the region defined by this widget, or when the widget disappears from
  /// under the pointer.
  final PointerExitEventListener onPointerExit;

  /// Called when a pointer that triggered an [onPointerDown] is no longer in
  /// contact with the screen.
  final PointerUpEventListener onPointerUp;

  /// Called when the input from a pointer that triggered an [onPointerDown] is
  /// no longer directed towards this receiver.
  final PointerCancelEventListener onPointerCancel;

  /// Called when a pointer signal occurs over this object.
  final PointerSignalEventListener onPointerSignal;

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

  // The widget listened to by the listener.
  //
  // The reason why we don't expose it is that once the deprecated methods are
  // removed, Listener will no longer need to store the child, but will pass
  // the child to `super` instead.
  final Widget _child;
}

上面是Listener的源码,结合源码给出的注释以及属性的名称,我们可以知道,Listener就是用来监听DOWN、MOVE等原始事件的。

根据RawGestureDetector的build函数,我们看到RawGestureDetector持有了一个监听DOWN事件的Listener。这个可以理解,因为DOWN事件是事件流的起始。

void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (GestureRecognizer recognizer in _recognizers.values)
    recognizer.addPointer(event);
}

上面是RawGestureDetector在遇到DOWN事件的回调,_recognizers是GestureDetector定义的一系列手势。那么这个函数的逻辑就很清楚了:每次遇到一个事件流的开始,RawGestureDetector就将这个事件通知给所有的手势定义类中,由它们进行后续的判定。

void addPointer(PointerDownEvent event) {
  _pointerToKind[event.pointer] = event.kind;
  if (isPointerAllowed(event)) {
    addAllowedPointer(event);
  } else {
    handleNonAllowedPointer(event);
  }
}

继续追踪addPointer函数,_pointToKind是一个Map,用来存储输入设备,不是我们关心的重点。isPointerAllowed函数用来判断手势是否接收这个事件,接受与否要根据手势的定义以及手势当前的状态决定。如果手势可以接收这个事件,那么就调用addAllowedPointer函数来改变状态并继续追踪事件流;否则调用handleNonAllowedPointer来拒绝这个事件。这三个函数被不同的GestureRecognizer分别进行实现,从而实现了不同手势的判断。

2.2.手势的竞争

只有手势的判断并不能完全解决问题,很多手势都需要接收一系列的MOVE事件,要想保证手势之间不发生冲突,就要解决手势处理事件的优先级问题。

@protected
void startTrackingPointer(int pointer) {
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  _entries[pointer] = _addPointerToArena(pointer);
}

无论是什么GestureRecognizer,最终都会在调用继承的OneSequenceGestureRecognizer类的startTrackingPointer函数。顾名思义,这个方法就是用来追踪事件流的。GestureBinding中的PointerRouter是一个Map,它的key是事件流的标识pointer,value是手势的一系列回调。_trackedPointers是一个HashSet,记录这个GestureRecognizer目前都正在追踪哪些事件流。_entries也是一个Map,key值同样是pointer,value值是一个GestureArenaEntry类。如同HitTestEntry一样,GestureArenaEntry类也是一个封装类,它由GestureArenaManager、pointer和GestureArenaMember组成。pointer是事件流的标识,GestureArenaMember是GestureRecognizer,在GestureArenaEntry类里起始就是这个GestureRecognizer自己。

pointerRouter我们先不管,_trackedPointers.add(pointer)也很好理解,我们先看最后的_addPointerToArena函数。

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null)
    return _team.add(pointer, this);
  return GestureBinding.instance.gestureArena.add(pointer, this);
}

GestureBinding的gestureArena是一个GestureArenaManager类。它实际上就是维护了一个Map。这个Map的key值仍然是GestureRecognizer正在追踪的事件流,value是一个_GestureArena类。这个类也很简单,就是维护一个GestureArenaMember的List。GestureBinding.instance.gestureArena.add这个函数的实现逻辑,我们可以用下面的这个比喻去理解:

一个GestureRecognizer是一个竞技场斗士,它正在追踪的事件流的编号可以理解为是这个斗士被分配的组,每一组都是都有一个竞技场_GestureArena。GestureArenaManager是比赛的委员会,它通过add函数来将每一位斗士按照被分配的组引导到对应的竞技场中。在同一个竞技场中的都是需要进行比赛,赢的人就可以获得事件的处理权。

但是委员会怎么知道冠军要怎么处理事件呢?我们再看startTrackingPointer函数中的GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);这句话。原来斗士要想进入竞技场,就必须先把自己怎么处理事件告诉委员会才行。

接下来就是正式的竞争了。

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

由于GestureBinding是宏观上分发所有事件的角色,所以我们还是要总它的handleEvent开始看。从函数中可以看到,一旦遇到了DOWN事件,就会调用GestureArenaManager的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);
}

close函数的原理很简单,首先判断这个事件流是不是正常的,如果是就关闭竞技场并且开始竞争。

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

_tryToResolveArena的逻辑也很简单。如果竞技场没有人,那么直接关闭这个竞技场并返回。如果竞技场只有一个人,那么冠军就直接产生了。如果竞技场有多个人,那么就要解决优先级问题了。解决优先级问题的方式也很简单——打假赛。当eagerWinner不为空的时候,说明这些GestureRecognizer中有一个高优先级的,这种情况下就不用比赛了,直接让他胜出就好了。

当然,大多数情况下GestureRecognizer的优先级都是相同的。这种情况也不需要我们特别操心,因为目前只处理了DOWN事件,通过后续的事件,还是可以让对应手势的GestureRecognizer坚持到最后的。

3.总结

说了这么多,可能大家关于Flutter的事件机制还是没有一个特别清楚的认识,所以在这里我们再简单总结一下

1.GestureDetector定义了一系列手势。由于所有的手势的第一个事件都是DOWN,所以只有DOWN事件产生后,所有手势的GestureRecognizer才会被激活。

2.GestureRecognizer被激活后,首先要看是否存在高优先级的GestureRecognizer。被激活的GestureRecognizer全部被加入到GestureBinding的gestureArena中。gestureArena通过事件流的唯一标识pointer来将被激活的GestureRecognizer进行分组

3.所有的Widget都继承自RenderBox类。当DOWN事件产生时,所有的Widget首先判断事件是否发生在自己的区域内,如果是,那么先通过递归判断自己子控件是否在监听事件,如果有,那么就将子控件和自己加入这个事件的处理链;如果没有那么再看自己是不是在监听事件,是的话就只将自己加入处理链。这个过程结束后,就会形成一条可能会响应该事件流的处理链。MOVE、UP、CANCEL事件都会复用DOWN事件产生的处理链。

4.事件链产生完毕后,Flutter认为所有被激活的GestureRecognizer都应该被分组了,这时关闭对应的事件流的分组,开始判断优先级。

5.少数情况下,事件流只激活了一个GestureRecognizer或者激活了一个高优先级的GestureRecognizer,亦或者没有激活GestureRecognizer,这样就解决了同一时刻多个控件征用事件冲突的问题。

6.大多数情况下, 不存在事件冲突的问题。事件流后续的事件通过每个GestureRecognizer的addPointer函数来判断是否符合手势定义。如果是,那么调用addAllowedPointer函数来响应事件;否则调用addNonAllowedPointer函数来拒绝事件。

7.addAllowedPointer会根据接受的事件决定GestureRecognizer的新状态,如果是手势的结束状态,那么就接受这个手势,并调用响应的回调;否则继续等待下一个事件。

 

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享