概述
不管是原生Android、iOS還是JavaScript,只要是涉及手勢互動都會有事件的分發處理。和原生Android、iOS的事件分發的步驟和原理一樣,Flutter的事件分發總體也由手勢觸發、攔截和響應等幾個部分構成。Flutter所有事件源頭是 hooks.dart檔案的_dispatchPointerDataPacket函式,通過攔截螢幕的點選、滑動等各種事件,進而分發給原生程式碼進行響應(ps:Android事件分發)。
如果你看過了解原生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包中,如下圖所示。
- converter.dart將物理座標_dispatchPointerDataPacket收到的物理資料PointerDataPacket轉換成PointerEvent, 類似於安卓在ViewRootImpl.java將InputEventReceiver收到的InputEvent轉換為MotionEvent。
- recognizer.dart的GestureRecognizer是所有手勢識別的基類。
- rendering/binding.dart的RendererBinding類關聯了render樹和Flutter引擎,等價於安卓的Surface。
- 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;
}
這個方法的作用就是判斷包含Padding的檢視是否在點選範圍內,如果命中,則阻止其他事件繼續冒泡。看到此處,我們大體可以看出,Flutter的事件處理主要是判斷點選的座標知否在控制元件範圍內,如果在範圍內直接響應,如果不在繼續向上冒泡,並且事件是從葉子開始的,也即Web中的事件冒泡。
完成命中處理後,接下來回到事件處理的主流程,即事件派發dispatchEvent,程式碼位於gestrues/binding裡面,原始碼如下。
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
assert(!locked);
// No hit test information implies that this is a hover or pointer
// add/remove event.這種情況出在指標懸停螢幕上方,微微接觸或不接觸,是手機敏感而言
if (hitTestResult == null) {
assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
event: event,
hitTestEntry: null,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
return;
}
for (HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a pointer event'),
event: event,
hitTestEntry: entry,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
}
}
此方法最根本的作用是迴圈事件分發,並以冒泡的形式從底部到分發事件,當事件被命中時,即由當前子節點處理事件,這和Android的事件分發的邏輯是一樣的。下面以GestureDetector和Listener來舉例事件分發的不同。如果用Listener的話,Listener的元件最終對應的RenderObject是RenderPointerListener,它的監測當前點選是否命中的方法如下。
bool hitTest(BoxHitTestResult result, { Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
使用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);
}
如果使用的是GestureDetector的話,build方法會為我們新增很多處理手勢的方法類,如TapGestureRecognizer
,通過處理手勢識別後,最終返回的是RawGestureDetector
,涉及的程式碼如下。
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
if (
onTapDown != null ||
onTapUp != null ||
onTap != null ||
onTapCancel != null ||
onSecondaryTapDown != null ||
onSecondaryTapUp != null ||
onSecondaryTapCancel != null
) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel;
},
);
}
if (onDoubleTap != null) {
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) {
instance
..onDoubleTap = onDoubleTap;
},
);
}
if (onLongPress != null ||
onLongPressUp != null ||
onLongPressStart != null ||
onLongPressMoveUpdate != null ||
onLongPressEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPress = onLongPress
..onLongPressStart = onLongPressStart
..onLongPressMoveUpdate = onLongPressMoveUpdate
..onLongPressEnd =onLongPressEnd
..onLongPressUp = onLongPressUp;
},
);
}
if (onVerticalDragDown != null ||
onVerticalDragStart != null ||
onVerticalDragUpdate != null ||
onVerticalDragEnd != null ||
onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = onVerticalDragDown
..onStart = onVerticalDragStart
..onUpdate = onVerticalDragUpdate
..onEnd = onVerticalDragEnd
..onCancel = onVerticalDragCancel
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onHorizontalDragDown != null ||
onHorizontalDragStart != null ||
onHorizontalDragUpdate != null ||
onHorizontalDragEnd != null ||
onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this),
(HorizontalDragGestureRecognizer instance) {
instance
..onDown = onHorizontalDragDown
..onStart = onHorizontalDragStart
..onUpdate = onHorizontalDragUpdate
..onEnd = onHorizontalDragEnd
..onCancel = onHorizontalDragCancel
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onPanDown != null ||
onPanStart != null ||
onPanUpdate != null ||
onPanEnd != null ||
onPanCancel != null) {
gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(debugOwner: this),
(PanGestureRecognizer instance) {
instance
..onDown = onPanDown
..onStart = onPanStart
..onUpdate = onPanUpdate
..onEnd = onPanEnd
..onCancel = onPanCancel
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
() => ScaleGestureRecognizer(debugOwner: this),
(ScaleGestureRecognizer instance) {
instance
..onStart = onScaleStart
..onUpdate = onScaleUpdate
..onEnd = onScaleEnd;
},
);
}
if (onForcePressStart != null ||
onForcePressPeak != null ||
onForcePressUpdate != null ||
onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = onForcePressStart
..onPeak = onForcePressPeak
..onUpdate = onForcePressUpdate
..onEnd = onForcePressEnd;
},
);
}
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
並且,RawGestureDetector預設使用的也是Listener,它註冊了手指按下的方法,分發的時候Down事件是sdk預設處理的。
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (GestureRecognizer recognizer in _recognizers.values)
recognizer.addPointer(event);
}
此方法會向Binding路由器中註冊那些需要處理的事件,假如我們只宣告瞭點選事件,那麼集合中負責新增的GestureRecognizer的實現類就是TapGestureRecognizer,接下來我們看一下addPointer方法。
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {
case kPrimaryButton:
if (onTapDown == null &&
onTap == null &&
onTapUp == null &&
onTapCancel == null)
return false;
break;
case kSecondaryButton:
if (onSecondaryTapDown == null &&
onSecondaryTapUp == null &&
onSecondaryTapCancel == null)
return false;
break;
default:
return false;
}
return super.isPointerAllowed(event);
}
isPointerAllowed方法的作用就是用來判定當前的手勢,預設返回false,如果事件比命中,接下來執行addAllowedPointer方法,如下所示。
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer, event.transform);
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;
primaryPointer = event.pointer;
initialPosition = OffsetPair(local: event.localPosition, global: event.position);
if (deadline != null)
_timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
}
void startTrackingPointer(int pointer, [Matrix4 transform]) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
這兩個方法的主要作用就是用來將當前的handleEvent方法新增到GestureBinding路由器裡面去,而_addPointerToArena是就是新增處理事件的具體邏輯。接下來,我們來看一下GestureBinding裡面的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);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(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單個控制元件消耗事件差不多,所以巢狀滾動總是先滾動內層,先被內層消耗,然後再執行外層。
參考: Flutter 事件分發