Flutter事件與手勢識別

Flutter的事件源

Flutter的原始事件是由window中 PointerDataPacketCallback(PointerDataPacket packet) 回調得到的,這個回調再GestureBinding初始化中就設置了window.onPointerDataPacket = _handlePointerDataPacket,咱們看一下_handlePointerDataPacket的代碼ide

void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  //_pendingPointerEvents是一個PointerEvent的隊列,這段代碼的意思是將PointerDataPacket轉換成PointerEvent而後存在隊列中
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();
}

//對隊列中的PointerEvent進行出隊並依次處理
void _flushPointerEventQueue() {
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
    _handlePointerEvent(_pendingPointerEvents.removeFirst());//_handlePointerEvent對每個PointerEvent進行處理
}
複製代碼

_handlePointerEvent方法纔是對每一個PointerEvent進行處理的地方post

void _handlePointerEvent(PointerEvent event) {
  HitTestResult hitTestResult;
  if (event is PointerDownEvent) {
    assert(!_hitTests.containsKey(event.pointer));
    hitTestResult = HitTestResult();
    hitTest(hitTestResult, event.position);//hitTest方法,來肯定命中測試的結果
    _hitTests[event.pointer] = hitTestResult;//event.pointer是每次連續的PointEvent的惟一id,以id爲key將hitTestResult存到_hitTests中
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    hitTestResult = _hitTests.remove(event.pointer);//事件結束標記,將hitTestResult從_hitTests取出並移除
  } else if (event.down) {
    hitTestResult = _hitTests[event.pointer];//move事件直接重用down事件的hitTestResult,避免每次都進行命中測試
  }
  if (hitTestResult != null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    dispatchEvent(event, hitTestResult);//分發Event
  }
}
複製代碼

_handlePointerEvent中比較重要的兩點:測試

  1. 在PointerDownEvent事件時,經過hitTest方法來計算出HitTestResult
  2. 對事件序列經過dispatchEvent(event, hitTestResult)進行分發事件

上面的過程就是將原始事件轉換成咱們須要的PointEvent,而後再肯定命中測試結果,最後再進行分發事件。ui

肯定HitTestResult

HitTestResult中有一個List _path的字段,由多個HitTestEntry來組成的path(事件進行冒泡的路徑,爲何是經過冒泡後面會有解釋),HitTestEntry是每個命中的入口,它只有一個HitTestTarget target字段,而HitTestTarget又是由RenderObject來實現的,因此HitTestResult其實就是一系列經過命中測試的RenderObject的集合。咱們來看看是如何來肯定命中測試的結果的this

void hitTest(HitTestResult result, Offset position) {
  assert(renderView != null);
  renderView.hitTest(result, position: position);
  super.hitTest(result, position);//這裏是調用的GestureBinding中的hitTest方法,將WidgetsFlutterBinding加入到最後面
}
複製代碼

調用renderView的hitTest方法,繼續跟進spa

bool hitTest(HitTestResult result, { Offset position }) {
  if (child != null)
    child.hitTest(result, position: position);
  result.add(HitTestEntry(this));
  return true;
}
複製代碼

能夠看到renderView並無直接實現HitTestable中的hitTest方法,renderView的hitTest方法中的{ Offset position }是一個可選參數,而且帶一個bool類型的返回值,renderView的hitTest方法顯示對child進行命中測試,讓後再將本身添加到命中測試結果。debug

RenderObject中並無發現hitTest方法,可是再其子類RenderBox中發現了名爲hitTest的方法,也沒有直接實現HitTestable中的hitTest方法,{ Offset position }也是一個可選參數,也有一個bool類型的返回值code

bool hitTest(HitTestResult result, { @required Offset position }) {
  if (_size.contains(position)) {//肯定hit的位置再本身的size範圍裏面
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
	  //先對children進行hitTest,而後再對本身進行hitTest,有一項返回true才能將本身添加到HitTestResult裏面
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}
複製代碼

咱們看一個比較簡單的例子,RenderPadding中是怎樣對children進行命中測試的,RenderPadding的hitTestChildren實如今RenderShiftedBox中,hitTestSelf的實如今RenderBox中router

@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
  if (child != null) {
    final BoxParentData childParentData = child.parentData;
    return child.hitTest(result, position: position - childParentData.offset);//將點擊點減去偏移應用到child的命中測試
  }
  return false;
}

@protected
bool hitTestSelf(Offset position) => false;//本身進行命中測試
複製代碼

RenderPadding的命中測試結果就是若是child命中測試成功,則本身也會被添加的命中測試結果中,不然就不對本身進行命中測試對象

分發Event

接下來就是對Event的分發了,咱們直接看GestureBinding中的dispatchEvent方法

@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
  assert(!locked);
  //沒有命中測試信息意味着PointerEvent是Hover,Added,Removed其中一種
  if (hitTestResult == null) {
    assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
    try {
	  //將事件分發到註冊了這次事件的路由,通常是由GestureRecognizer中void addPointer(PointerDownEvent event)方法進行註冊
      pointerRouter.route(event);
    } catch (exception, stack) {}
    return;//進行路由分發直接返回
  }
  //對命中測試的結果進行遍歷,應爲是先對child進行命中測試,因此事件的序列是冒泡向上傳遞的
  for (HitTestEntry entry in hitTestResult.path) {
    try {
	  //調用target的handleEvent方法處理事件
      entry.target.handleEvent(event, entry);
    } catch (exception, stack) {}
  }
}
複製代碼

當命中測試結果爲空時進行路由分發,當命中測試結果不爲空時,就進行命中結果分發,handleEvent方法的實現咱們來看一個比較典型的,RenderPointerListener中的handleEvent,RenderPointerListener時Listener(能夠監聽原始PointEvent的Widget)對應的RenderObject對象

@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
  assert(debugHandleEvent(event, entry));
  // onPointerEnter, onPointerHover, and onPointerExit events都在MouseTracker裏面處理
  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);
}
複製代碼

RenderPointerListener直接把事件給到對應的回調,大多數RenderObject都沒有實現handleEvent方法。

事件監聽

Flutter的官方文檔推薦咱們使用GestureDetector來檢測用戶手勢輸入,GestureDetector幫咱們區別了各類類型的手勢,咱們只須要設置須要監聽的手勢回調就能夠了,使用很是方便。 從咱們上面的分析能夠看到,事件的產生與分發,咱們來看一下GestureDetector是如何監聽事件並進行區別手勢的呢?

@override
Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
	...省略若干代碼
  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}
複製代碼

能夠看到GestureDetector是經過RawGestureDetector來實現的,咱們再看RawGestureDetector

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

而RawGestureDetector又是經過Listener來實現的,上面咱們知道Listener是監聽初始事件PointerEvent的,那他是如何被區別爲各類各樣的手勢的呢?

手勢識別

看一下RawGestureDetector中的_handlePointerDown方法

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

在PointerDownEvent的時候,將全部recognizer進行addPointer(event),繼續跟進addPointer方法,在GestureRecognizer中是空實現,咱們先看一個簡單的實現TapGestureRecognizer

@override
void addPointer(PointerDownEvent event) {
  startTrackingPointer(event.pointer);//開始追蹤id爲pointer的事件序列
  if (state == GestureRecognizerState.ready) {
    state = GestureRecognizerState.possible;
    primaryPointer = event.pointer;
    initialPosition = event.position;
    if (deadline != null)
      _timer = Timer(deadline, didExceedDeadline);
  }
}
複製代碼

addPointer中最主要的方法就是startTrackingPointer方法,這個方法是OneSequenceGestureRecognizer中的,可讓OneSequenceGestureRecognizer去追蹤這個事件序列,具體分析在下面手勢競技中再講,Recognizer追蹤了這個事件序列後,這個事件的後續事件都會被這個Recognizer處理,會觸發handleEvent方法,經過一系列判斷會走到handlePrimaryPointer方法,而後再PointerUpEvent時觸發 相關回調

@override
void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _finalPosition = event.position;
    _checkUp();
  } else if (event is PointerCancelEvent) {
    _reset();
  }
}

void _checkUp() {
  if (_wonArenaForPrimaryPointer && _finalPosition != null) {
    resolve(GestureDisposition.accepted);
    if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
      return;
    }
    if (onTapUp != null)
      invokeCallback<void>('onTapUp', () { onTapUp(TapUpDetails(globalPosition: _finalPosition)); });//觸發onTapUp回調
    if (onTap != null)
      invokeCallback<void>('onTap', onTap);//觸發onTap回調
    _reset();
  }
}
複製代碼

Recognizer會對事件進行分析,而後會去區別不一樣的狀況去觸發不一樣的回調。

若是一個事件序列被多個Recognizer追蹤,好比須要監聽用戶點擊與滑動,那麼怎麼去區別用戶究竟是點擊仍是滑動呢?

手勢競技

咱們先看一下Recognizer是如何追蹤事件序列的,先看startTrackingPointer方法

@protected
void startTrackingPointer(int pointer) {
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  _entries[pointer] = _addPointerToArena(pointer);
}
複製代碼
  1. GestureBinding(實例是WidgetsFlutterBinding)中的PointerRouter,其實就是維護了一個Map<int,LinkedHashSet> routerMap(路由表)的屬性,PointerRoute其實就是void Function(PointerEvent event)類型的回調。從上面能夠看到,將pointer做爲key,handleEvent方法做爲值傳入。
  2. _trackedPointers是一個HashSet,記錄此Recognizer追蹤的事件序列
  3. _entries是一個Map<int, GestureArenaEntry>,GestureArenaEntry中包含一個GestureArenaManager(手勢競技管理類)、_pointer(事件id)、GestureArenaMember(GestureRecognizer的基類,其實就是Recognizer自己)

繼續看一下_addPointerToArena方法

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

能夠看到GestureBinding(實例是WidgetsFlutterBinding)中的gestureArena字段(它是GestureArenaManager)將這個Recognizer添加進去。看看其add方法

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

GestureArenaEntry add(int pointer, GestureArenaMember member) {
  //_GestureArena中是一個GestureArenaMember的list,其實就是一個手勢id,對應多個GestureArenaMember
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    return _GestureArena();
  });
  state.add(member);//將GestureArenaMember添加到_GestureArena中
  return GestureArenaEntry._(this, pointer, member);//再返回一個GestureArenaEntry對象給Recognizer中entries持有
}
複製代碼

從以上的分析能夠總結一下startTrackingPointer主要作的事情:

  1. 跟據手勢的id(pointer)來添加路由,此Recognizer就能夠接受處理餘下的事件序列,當有餘下事件序列發送過來就會調用此Recognizer中的handleEvent方法(此功能由GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent)代碼實現)
  2. 將Recognizer添加到手勢競技場,處理同一個手勢id的Recognizer將被添加到同一個_GestureArena(手勢競技場)中,(此功能由_addPointerToArena方法實現)

上面只是添加到路由以及競技場,可是咱們還不知道是事件是怎樣被髮送到指定的路由,以及多個手勢識別器是如何競爭處理手勢事件的

事件路由到指定Recognizer

咱們知道事件的傳遞是經過冒泡來進行傳遞的,HitTestResult的最上層是WidgetsFlutterBinding,最後處理事件的應該在GestureBinding中,咱們看一下GestureBinding的handleEvent方法

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);//調用PointerRouter的route方法將事件進行路由
  ...省略
}
複製代碼

GestureBinding的dispatchEvent方法在HitTestResult爲null的時候才路由,主要也就是Hover,Added,Removed這三種事件進行路由,而處理這三個的是一個global的MouseTracker。而此處路由會處理全部註冊到routerMap中的Recognizer(實際上只是其handleEvent方法)。

多個Recognizer競技

仍是看GestureBinding的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);
  }
}
複製代碼

經過pointerRouter路由以後,在PointerDownEvent時候就調用GestureArenaManager的close方法(防止其餘Recognizer加入到手勢競技中),因此在就是爲何addPoint註冊路由的方法須要PointerDownEvent做爲參數了,一旦在down的時候不註冊,那麼這個事件就與你的Recognizer無關了。看一下close方法

void close(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return;
  state.isOpen = false;
  _tryToResolveArena(pointer, state);
}
複製代碼

先將isOpen變爲false(在add的時候首先就會判斷isOpen),而後調用_tryToResolveArena方法,繼續跟進

void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(!state.isOpen);
  if (state.members.length == 1) {//只有1個Recognizer,直接添加一個_resolveByDefault方法調用的task
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {//沒有Recognizer,直接移除該pointer對應的_GestureArena
    _arenas.remove(pointer);
  } else if (state.eagerWinner != null) {//渴望的勝利者,在_GestureArena關閉的時候若是不爲空會直接做爲勝利者
    _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);//將不是勝利者的GestureArenaMember所有調用拒絕手勢
  }
  member.acceptGesture(pointer);//調用勝利這的接受手勢
}
複製代碼

可是咱們在沒有eagerWinner的時候是怎樣來競技的呢?咱們用兩個具體的手勢識別器點擊(TapGestureRecognizer)、滑動(PanGestureRecognizer)來分析

首先咱們滑動一下

通過上面的分析,在down的時候是解析不出勝利者的,後續move事件會路由給TapGestureRecognizer,PanGestureRecognizer,咱們須要看一下TapGestureRecognizer的handleEvent方法

@override
void handleEvent(PointerEvent event) {
  assert(state != GestureRecognizerState.ready);
  if (event.pointer == primaryPointer) {
	//接受手勢前滑動距離是否溢出容忍值
    final bool isPreAcceptSlopPastTolerance =
        state == GestureRecognizerState.possible &&
        preAcceptSlopTolerance != null &&
        _getDistance(event) > preAcceptSlopTolerance;
	//接受手勢後滑動距離是否溢出容忍值
    final bool isPostAcceptSlopPastTolerance =
        state == GestureRecognizerState.accepted &&
        postAcceptSlopTolerance != null &&
        _getDistance(event) > postAcceptSlopTolerance;
	//move事件下,超出容忍值
    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
      resolve(GestureDisposition.rejected);//拒絕後續事件
      stopTrackingPointer(primaryPointer);//結束追蹤事件
    } else {
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}
複製代碼

因此但凡咱們滑動的距離超出了容忍值(這個值是根據經驗事件來肯定的值),都會拒絕事件結束追蹤。因此事件就會落到PanGestureRecognizer身上。

咱們再點一下

咱們須要看一下PanGestureRecognizer的handleEvent方法

@override
void handleEvent(PointerEvent event) {
  assert(_state != _DragState.ready);
  if (!event.synthesized
      && (event is PointerDownEvent || event is PointerMoveEvent)) {
    final VelocityTracker tracker = _velocityTrackers[event.pointer];
    assert(tracker != null);
    tracker.addPosition(event.timeStamp, event.position);
  }

  if (event is PointerMoveEvent) {
    final Offset delta = event.delta;
    if (_state == _DragState.accepted) {
      if (onUpdate != null) {
        invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
          sourceTimeStamp: event.timeStamp,
          delta: _getDeltaForDetails(delta),
          primaryDelta: _getPrimaryValueFromOffset(delta),
          globalPosition: event.position,
        )));
      }
    } else {//move事件,沒有接受事件時
      _pendingDragOffset += delta;
      _lastPendingEventTimestamp = event.timeStamp;
	  //判斷是否由足夠的滑動距離來接受,也就是說滑動距離超過必定距離會主動接受
      if (_hasSufficientPendingDragDeltaToAccept)
        resolve(GestureDisposition.accepted);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);//up事件的時候會中止追蹤
}
複製代碼

因爲咱們是點一下,那麼距離不夠是不會去主動接受的,等通過一系列move事件結束後PanGestureRecognizer仍是沒有得到事件,最後再up的時候就中止追蹤事件了,那麼事件就會落到TapGestureRecognizer身上。

經過上面兩種狀況的分析,不一樣的Recognizer都有本身的邏輯去接受、拒絕、中止追蹤事件。

接受、拒絕、中止追蹤事件

經過resolve方法傳入一個GestureDisposition可讓Recognizer來處置事件,咱們跟進resolve方法看一下具體操做,

void resolve(GestureDisposition disposition) {
  final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
  _entries.clear();
  for (GestureArenaEntry entry in localEntries)
    entry.resolve(disposition);
}

void resolve(GestureDisposition disposition) {
  _arena._resolve(_pointer, _member, disposition);
}

void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; 
  assert(state.members.contains(member));
  if (disposition == GestureDisposition.rejected) {
    state.members.remove(member);//移除一個Recognizer
    member.rejectGesture(pointer);//調用其rejectGesture方法
    if (!state.isOpen)
      _tryToResolveArena(pointer, state);//嘗試肯定勝利者
  } else {
    if (state.isOpen) {
      state.eagerWinner ??= member;
    } else {
      _resolveInFavorOf(pointer, state, member);//肯定勝利者
    }
  }
}
複製代碼

能夠看到若是是接受,就直接確認勝利者,若是是拒絕,就將其踢出並嘗試確認勝利者。再看一下stopTrackingPointer的具體操做

void stopTrackingPointer(int pointer) {
  if (_trackedPointers.contains(pointer)) {
    GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);//移除路由,不處理餘下事件
    _trackedPointers.remove(pointer);//從_trackedPointers中移除
    if (_trackedPointers.isEmpty)
      didStopTrackingLastPointer(pointer);
  }
}
複製代碼

主要就是移除路由,didStopTrackingLastPointer是在沒有追蹤的PointEvent時,作一些收尾工做,具體都有不一樣實現。

相關文章
相關標籤/搜索