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作一些清理或者狀態重置的工做。

相關文章
相關標籤/搜索