Flutter中的事件流和手势简析

事件流

在你点击按钮,滑动列表,缩放图片等等交互过程当中,在背后却有成千上百的事件触发,如何处理这些事件?如何掌控事件的流动?不管在web, android或者ios,都是学习的一个难点,在Flutter同理也是同样,究竟Flutter的事件流有啥特别之处,接下来就慢慢展现给你们。android

从根源开始

事件从哪里来?通常来讲都不须要应用开发者去担忧事件是如何从硬件收集起来的,可是事件的传递总须要有个源头。
在Flutter里面主要处理事件和手势相关的就在gestures文件夹下。
而Flutter框架事件的源头就在gestures/binding.dart里的GestureBinding类开始:ios

void initInstances() {
    super.initInstances();
    _instance = this;
    ui.window.onPointerDataPacket = _handlePointerDataPacket;
  }

可见事件是由ui.window.onPointerDataPacket产生,把事件传给GestureBinding._handlePointerDataPacket方法,而ui.window这个就是sky引擎的实现,之后有机会再去深刻,如今只需关注上层。
纵观整个代码,会发现有不少binding,SchedulerBinding,GestureBinding,ServicesBinding,RendererBinding和WidgetsBinding等都跟引擎相关的,之后再慢慢逐个分析。
接着继续跟踪方法的调用过程:web

clipboard.png

先看_handlePointerEvent方法:浏览器

void _handlePointerEvent(PointerEvent event) {
    assert(!locked);
    HitTestResult result;
    if (event is PointerDownEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      result = new HitTestResult();
      hitTest(result, event.position);
      _hitTests[event.pointer] = result;
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $result');
        return true;
      }());
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      result = _hitTests.remove(event.pointer);
    } else if (event.down) {
      result = _hitTests[event.pointer];
    } else {
      return;  // We currently ignore add, remove, and hover move events.
    }
    if (result != null)
      dispatchEvent(event, result);
  }

当是PointerDownEvent事件的时候,会新建一个HitTestResult对象,而这个HitTestResult对象里面有一个path的属性,能够推测这个属性就是用来记录事件传递所通过的的节点。
新建HitTestResult对象后,接下来重点就是调用GestureBinding.histTest方法。
在看看hitTest方法:app

void hitTest(HitTestResult result, Offset position) {
    result.add(new HitTestEntry(this));
  }

这里把自身添加到HitTestResult上,意味着之后dispatchEvent时候会遍历path上的HitTestEntry,也会调起GestureBinding.handleEvent方法。
接着再看handleEvent方法:框架

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

这里就看到pointerRouter路由事件,以及手势相关的一些处理,手势等会再说。
可是看完整个方法调用都没看到事件是如何传递到节点树上,而pointerRouter仅仅是一个观察者模式的实现,找遍了代码也没找到对应的listener,事件是如何传递?咱们的点击事件是如何响应?依然不清楚。ide

柳暗花明

既然GestureBinding上并无事件如何传递节点树的实现,再看哪里用到这个类,总有地方须要依赖它的。
很快就注意到WidgetsFlutterBinding这个类了。学习

class WidgetsFlutterBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      new WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

WidgetsFlutterBinding这个类mixin了好几个Binding,同时这个类也是框架的初始化入口,当咱们跑起整个Flutter应用时:ui

void main() {
  runApp(new MyApp());
}

runApp其实就会执行WidgetsFlutterBinding.ensureInitialized方法初始化各个Binding:this

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

而后attachRootWidget方法,就去设置根节点了:

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
  }

这里看到了真正的根节点renderView,事件怎样也应该从根节点开始传递吧。
沿着renderView就找到RndererBinding.hitTest方法:

void hitTest(HitTestResult result, Offset position) {
    assert(renderView != null);
    renderView.hitTest(result, position: position);
    // This super call is safe since it will be bound to a mixed-in declaration.
    super.hitTest(result, position); // ignore: abstract_super_member_reference
  }

到这里基本能够肯定先调用RendererBinding.histTest方法接着调用GestureBinding.histTest方法。
再回头GestureBinding的实现也就是先让renderView.hitTest方法去肯定事件传递路径,都添加到HitTestResult的path上,最后再添加GestureBinding自身做为最后的一个HitTestEntry。
而GestureBindg.dispatchEvent会遍历这些HitTestEntry调用他们的handleEvent方法:

void dispatchEvent(PointerEvent event, HitTestResult result) {
    assert(!locked);
    assert(result != null);
    for (HitTestEntry entry in result.path) {
      try {
        entry.target.handleEvent(event, entry);
      } catch (exception, stack) {
        FlutterError.reportError(new FlutterErrorDetailsForPointerEventDispatcher(
          exception: exception,
          stack: stack,
          library: 'gesture library',
          context: 'while dispatching a pointer event',
          event: event,
          hitTestEntry: entry,
          informationCollector: (StringBuffer information) {
            information.writeln('Event:');
            information.writeln('  $event');
            information.writeln('Target:');
            information.write('  ${entry.target}');
          }
        ));
      }
    }
  }

还有一个重点就是节点上hitTest方法实现,而节点通常都是继承自RenderBox的实现:

bool hitTest(HitTestResult result, { @required Offset position }) {
    if (_size.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(new BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
  }

固然首先判断点击是否在节点位置上,而后再交给children处理,接着自身处理,若是hitTestChildren或者hitTestSelf返回true,就把当前节点加入到HitTestResult上。
这个时候HitTestResult中的路径顺序通常就是:

目标节点-->父节点-->根节点-->GestureBinding

接着PointerDown,PointerMove,PointerUp,PointerCancel等事件分发,都根据这个顺序来遍历调用它们的handleEvent方法,就像浏览器事件的冒泡过程同样,既然像冒泡同样,搞过web开发的同窗都知道,浏览器是能够用代码阻止冒泡的,那Flutter行不行尼?答案,暂时尚未发现有方法能够阻止这个冒泡过程。

手势

如今已经清楚框架的事件流,如今开始深刻框架的手势系统。

GestureDector

The GestureDetector widget decides which gestures to attempt to recognize based on which of its callbacks are non-null.

根据文档所说GestureDetector控件能够检测手势,而且根据手势调起相应回调。

clipboard.png

GestureDector真的支持了至关多的手势,基本上经常使用都有了,框架实在太给力!

那GestureDector控件为何有这么大本领,而手势是如何检测的尼?

先对这个控件层层剥皮,看它的build方法:

Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    ...
    return new RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
}

能够看到GestureDector其实就是根据注册的回调,添加对应的GestureRecognizer(手势识别器),并全传递到RawGestureDetector。
而RawGestureDetector的build方法:

Widget build(BuildContext context) {
    Widget result = new Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child
    );
    if (!widget.excludeFromSemantics)
      result = new _GestureSemantics(owner: this, child: result);
    return result;
  }

关键在于_handlePointerDown方法:

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

遍历_recognizers(手势识别器)调用addPointer方法,通常来讲recognizer都是继承自PrimaryPointerGestureRecognizer的实现:

void addPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer);
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
      initialPosition = event.position;
      if (deadline != null)
        _timer = new Timer(deadline, didExceedDeadline);
    }
  }

到这里先理一下流程,当肯定PointerDown事件落在GestureDector控件下的子组件时,在GestureDector上注册的GesutreRecognizer就会追踪这个pointer(就是咱们的手指),注意了这里仍是设置一个Timer后面再说有什么做用,先看startTrackingPointer方法:

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

啊哈,这里用到了GestureBinding.instance.pointerRouter,还记得上面提到的吗,事件传递的最后一站其实就是GestureBinding,而后调用它的handleEvent方法,到最后就是调用pointer.route方法路由事件,因此还要调用GestureRecognizer的handleEvent方法。
接着再看GestureRcognizer._addPointerToArea方法

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

这里又用到GestureBinding.instance.gestureArena,其实就是GestureArenaManager,再看add方法:

GestureArenaEntry add(int pointer, GestureArenaMember member) {
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
      return new _GestureArena();
    });
    state.add(member);
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
    return new GestureArenaEntry._(this, pointer, member);
  }

这里就是新建了一个GestureArenaEntry对象,好吧,咱们得整理一下他们的关系:

class GestureArenaManager {
    final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
}

class _GestureArena {
    final List<GestureArenaMember> members = <GestureArenaMember>[];
}

class OneSequenceGestureRecognizer extends GestureArenaMember {
    final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
}

下面用个形象一点的方法描述它们的关系:
首先咱们有一批竞技选手(各类Recognizer),咱们也可能会有好几个竞技场地(_GestureArena),咱们的场地管理员(GestureArenaManager)会根据Pointer的多少来构建场地,可是各个选手也要拿到每一个竞技场的入场券(GestureArenaEntry)才能入场与其余选手一较高下。
当咱们的选手拿着对应的入场券进场后,如今各个场地都汇集了一批选手,叮的一声(PointerDown事件),各个场地入口关闭,过了一会激烈的竞技,又叮的一声(PointerUp事件)竞技结束,咱们就要打扫竞技场看一下哪一位选手胜利了。
这里PointerDown事件和PointerUp事件控制场地关闭和打扫,主要代码在GestureBinding.handleEvent方法上,上面就有提到,这里就不贴了。
那么怎么判断哪一个手势是最后赢得胜利留下来的呢,不像现实竞技场那么残酷,这里是很斯文优雅的,对手本身会判断是否要退出竞争,判断条件固然是PointerDown,PointerMove,PointerUp事件传递的信息是否符合当前手势的定义,若是不符合就自动退出,若是符合就向竞技场(_GestureArena)申请我符合条件,请判我获取胜利,其余手势只能判断为失败了。
可是这里也会有一些状况须要特别处理:

  • 若是参与者只有一个,或者其余参与者退出后只剩一个,就会让惟一剩下的参与为胜利
  • 若是没有手势请求获取胜利,竞技场也没被其余手势hold住,怎么办,那么竞技场调用sweep方法会让默认第一个手势会判断为胜利,其余判断为失败
  • 若是手势之间有冲突,例如一个DoubleTap和一个Tap,DoubleTap手势能够请求竞技场Hold住(等一下不要那么快打扫,判断优胜者),可是请求竞技场hold住的手势,必须以后主动请求竞技场release(好了,你能够打扫了),等DoubleTap手势决定是不是优胜仍是自动退出,就能够知道Tap手势是否最终生效,这样看Tap手势好像不会乱搞事情,就静静的等待全部对手退出,本身最终符合第一或者第二个条件,而判断为胜利。

因此整个竞技场的核心,只是仅仅让当前手势知道已经没有别的手势竞争,能够本身判断是否符合当前手势的定义而触发相应的事件,因此竞技场胜利的一方并非百分百触发手势的,得到竞技场胜利只是触发手势的必要非充分条件。
固然整个机制仍是有点出入的,下面还会继续分析。

举个栗子

例如TapGestureRecognizer,在不存在竞争的状况时,当GestureAreaManager.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);
  }

就会接着调起_tryToResolveArena方法:

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

由于是没有竞争者,因此就会跳进_resolveByDefault方法:

void _resolveByDefault(int pointer, _GestureArena state) {
    if (!_arenas.containsKey(pointer))
      return;  // Already resolved earlier.
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    final List<GestureArenaMember> members = state.members;
    assert(members.length == 1);
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
  }

这里最后就调起TapGestureRecognizer.acceptGesture方法:

void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown();
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
 }

_checkDown会尝试调起onTapDown,这里能够说onTapDown会当即调用,_checkUp会尝试调起onTapUp,onTap的回调(至少等onPointerUp事件触发才会成功)。

接下来咱们考虑若是父节点监听了Tap手势,也就是出现竞争状况,两个都是TapGestureRecognizer,状况会怎样的尼?

很明显GestureAreaManager.close方法中的_tryToResolveArena方法并无起到啥做用,这个时候你们还记得deadline这个超时时间吗,TapGestureRecognizer设置的超时时间为100毫秒,当咱们按下的时间超过100毫秒
TapGestureRecognizer.didExceedDeadline就会调用接着调起_checkDown方法(意味着onTapDown触发有可能延迟100毫秒,并不彻底是你点下的一瞬间就触发),可是咱们点击的时间很快(低于100毫秒)的时候又怎样尼?

别忘了在GestureAreaManager的方法处理以前,pointerRouter先会路由事件,直接调起 TapGestureRecognizer.handleEvent
-->TapGestureRecognizer.handlePrimaryPointer
-->TapGestureRecognizer._checkUp
-->TapGestureRecognizer.stopTrackingIfPointerNoLongerDown
-->TapGestureRecognizer.didStopTrackingLastPointer

既然咱们在上面事件流的分析知道,事件流就相似浏览器事件冒泡的方式,因此注册在pointerRouter的监听器也是子组件优先调用接着是父组件。接着stopTrackingIfPointerNoLongerDown方法将注册的监听器从pointerRouter移除,didStopTrackingLastPointer方法把TapGestureRecognizer的状态设置成ready,准备好下次手势处理。

这里再简单介绍一下GestureRecognizer的几个状态:

  • ready 初始状态准备好识别手势
  • possible 开始追踪pointer,不停接收路由的事件,若是中止追踪,则吧状态转回ready
  • defunct 手势已经被决议(accepted或者rejected)

最后就是打扫竞技场了:

void sweep(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return;  // This arena either never existed or has been resolved.
    assert(!state.isOpen);
    if (state.isHeld) {
      state.hasPendingSweep = true;
      assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
      return;  // This arena is being held for a long-lived member.
    }
    assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // First member wins.
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
      state.members.first.acceptGesture(pointer);
      // Give all the other members the bad news.
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }

默认会让第一个手势胜出,其余都会调起rejectGesture方法。可是在咱们刚才的举的例子已经不起做用了,由于手势都处理完毕,都回到ready状态了。

在看看若是是两个不一样类型的手势竞争的状况下会怎样,例如:TapGestureRecognizer 和 LongPressGestureRecognizer。
假设在GestureDector上同时注册了onTap和onLongPress。

这个时候GestureRecognizer注册的顺序就很重要了,在GestureDector里面框架已经设置好各自顺序,这里TapGestureRecognizer先于LongPressGestureRecognizer处理事件,由于最后处理手势的时候默认是第一个胜出的。

LongPressGestureRecognizer设置的超时时间为500毫秒,若是点击时间低于500毫秒时,就好像没有竞争状况同样,onTap回调正常调起,可是点击时间超过500毫秒,又会怎样尼?

这时就会调起LongPressGestureRecognizer.didExceedDeadline方法:

void didExceedDeadline() {
    resolve(GestureDisposition.accepted);
    if (onLongPress != null)
      invokeCallback<Null>('onLongPress', onLongPress); 
  }

而接着调起的就是GestureArenaManager._resolve方法:

void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return;  // This arena has already resolved.
    assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
    assert(state.members.contains(member));
    if (disposition == GestureDisposition.rejected) {
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen)
        _tryToResolveArena(pointer, state);
    } else {
      assert(disposition == GestureDisposition.accepted);
      if (state.isOpen) {
        state.eagerWinner ??= member;
      } else {
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
        _resolveInFavorOf(pointer, state, member);
      }
    }
  }

由于被决议为accepted,最后调起_resolveInFavorOf方法,至于eagerWinner的设置是在hitTest时候resolve才会起效。
再看_resolveInFavorOf方法:

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

直接reject了TapGestureRecognizer,TapGestureRecognizer的状态被设置为defunt,LongPressGestureRecognizer成为最后的优胜者。

总结

咱们能够在GestureRecognizer.handleEvent判断手势是否符合本身定义,例如滑动多少距离范围;设置deadline超时时间规定手势需在多少时间内完成,或者超出多少时间才符合定义;当检测到手势符合咱们定义或者不符合时,能够调起resolve决议,让其余手势识别放弃监听手势并重置状态;咱们自定义手势识别器应在rejectGesture作一些清理或者状态重置的工做。

相关文章
相关标签/搜索