一次触摸事件从Down开始Up结束, 而Flutter是一个跨平台的UI框架, 那么Flutter是如何收集不同平台传递的触摸事件?
我们以Android为例深入探索Flutter对触摸事件的处理.
点击事件响应流程
首先一个点击事件出发点肯定是从Native发出的, 然后经过多次转换传到Flutter层.
总结来说
Native -> C++
Android onTouchEvent 接收到触摸事件, 通过 Flutter.JNI 传递到 C++ 层.
C++ -> Dart
C++ 通过 RuntimeControll 控制器 调用 Window.DispatchPointerDataPacker 将窗体坐标点位传递到Dart层.
Dart -> 逻辑像素
Dart层通过FlutterWindow与Native交互, 通过GestureBinding#window.onPointDataPacket初始化, 开始进行手势处理.
Flutter处理点击事件流程
- 收集所有的触摸事件转换为逻辑像素.
- 对收集的点击事件进行命中测试, 得到一个新的集合
HitTestResult
. - 对
HitTestResult
收集的Widget进行触摸事件分发, 最后处理事件的是GestureBinding
, 宣告一次点击事件结束, 并将都能够响应触摸事件的Widget进行手势竞争, 胜出者接受手势, 其他接受拒绝手势.
触摸事件收集与分发
原始数据转换为逻辑像素
由Native传来的原始触摸事件会把由GestureBinding#_handlePointerDataPacket
方法转换为对应的逻辑像素.
// 需要处理的队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
// 将指针数据转化为逻辑像素
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
触摸事件处理
在GestureBinding#_handlePointerEvent
方法进行触摸事件的命中测试和分发.
// 记录所有的Down点击事件, 不会跟踪悬停状态的事件, 因为需要对每一帧进行命中测试
// key是event.pointer, 它不会重复, 每个Down事件的时候pointer会+1
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
void _handlePointerEvent(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
// Dwon事件触发
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
//进行命中测试
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
// 添加到 _hitTests 集合
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
// up 事件之后, 手势结束, 所以移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// 移动的过程, 根据event.pointer, 进行重新赋值
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
// 事件分发
dispatchEvent(event, hitTestResult);
}
}
一次点击事件由
Down+Up
组成, 经过Dart代码将屏幕实际位置转换为物理位置, 然后加入队列, 对Dwon事件进行hitTest(命中测试)
hitTest 对响应触摸事件的Widget进行汇总
hitTest
会从根Widget进行深度优先遍历, 把所有能够响应触摸事件的Widget添加到队列.
也就是说最后添加到Widget树的Widget会优先添加到队列进行命中测试.
而命中测试
就是判断触摸点位是否在Widget的Layout范围内(包顶部和左侧边缘, 不包底部和右侧边缘)
最终HitTestResult
汇总了所有能够响应点击事件的Widget集合, 而需要注意的是, GestureBinding
方法最终会把自己添加到队列末尾.
- GestureBinding#hitTest
// GestureBinding 会把自己添加到队列末尾
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
- RendererBinding#hitTest
/// 负责绘制渲染的 Root 节点
RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
///绘制树的owner,负责绘制,布局,合成
PipelineOwner get pipelineOwner => _pipelineOwner;
// RendererBinding 对 hitTest进行了重写
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
assert(result != null);
assert(position != null);
// 从根节点进行遍历
// 调用 -> View#hitTest
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
- View#hitTest
bool hitTest(HitTestResult result, { required Offset position }) {
// 深度优先遍历
// 调用 -> RenderBox#hitTest
if (child != null)
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
- RenderBox#hitTest
// 如果接受的触摸事件在Widget树中, 则返回 true
// position 是被转换之后的相对坐标
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// 判断触摸点位是否在Widget的上下左右范围内(包顶部和左侧边缘, 不包底部和右侧边缘)
if (_size!.contains(position)) {
// true: 当这个Widget的孩子或者自己包含触摸点, 则把添加这个绘制对象到hitResult中, 这样就说明当前Widget已经响应了触摸事件.
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
// false: 交给下一个Widget处理
return false;
}
手势分发
在GestureBinding#_handlePointerEvent
方法最后进行事件分发.
void _handlePointerEvent(PointerEvent event) {
// 深度遍历收集所有命中测试的Widget.
// ......
if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) {
assert(event.position != null);
// 对收集的Widget进行事件分发, Widget树最深的, 即UI最上层的Widget最先响应.
dispatchEvent(event, hitTestResult);
}
}
// 事件将发送到[HitTestResult]集合中的每个[HitTestTarget]handleEvent方法, 其中处理程序的所有异常都会被捕获
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
for (final HitTestEntry entry in hitTestResult.path) {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
总结
触摸事件主要的处理类是GestureBinding
, 它会对Widget树进行深度遍历, 并对每一个Widget进行测试命中
, 测试命中的判断依据是该触摸事件的坐标是否在WidgetLayout范围内, HitTestResult
负责收集所有测试命中的Widget, 并对其进行事件分发dispatchEvent
.
手势冲突
上面介绍了, GestureBinding#_handlePointerEvent
方法会对测试命中的Widget进行事件分发, 而很显然并不是测试命中的Widget就可以响应并处理触摸事件, 那么什么样的Widget会收到Down事件并且能够进行后续处理呢?
Down 事件进一步收集
以InkWell为例, 我们查看Down事件是如何被收集的.
// InkWell继承了InkResponse, 那么查看InkResponse的实现
class InkWell extends InkResponse {}
// InkResponse是一个StatelessWidget, 那么查看build方法
class InkResponse extends StatelessWidget {
Widget build() {
return
// 其他 ...
// 最后嵌套了GestureDetector
GestureDetector();
}
}
// 继续查看build方法
class GestureDetector extends StatelessWidget {
Widget build() {
return RawGestureDetector();
}
}
// 继续查看build方法
class RawGestureDetector extends StatefulWidget {
Widget build() {
return Listener();
}
}
// Listener最后嵌套的了RenderPointerListener
class Listener extends SingleChildRenderObjectWidget {
RenderPointerListener createRenderObject(BuildContext context) {
return RenderPointerListener();
}
}
// 最终发现 RenderPointerListener 间接实现了RenderObject, 而RenderObject实现了 HitTestTarget
// 还记得 GestureBinding#_handlePointerEvent 会遍历调用 HitTestTarget#handleEvent 方法吗
class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
// event 是 经过RawGestureDetector实现了Down事件传递过来的
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent)
return onPointerDown?.call(event);
if (event is PointerMoveEvent)
return onPointerMove?.call(event);
if (event is PointerUpEvent)
return onPointerUp?.call(event);
if (event is PointerHoverEvent)
return onPointerHover?.call(event);
if (event is PointerCancelEvent)
return onPointerCancel?.call(event);
if (event is PointerSignalEvent)
return onPointerSignal?.call(event);
}
}
// RawGestureDetector最终嵌套了RenderPointerListener, 而RenderPointerListener被HitTestResult#_handlePointerDown方法回调.
// 而在回调RawGestureDetector#_handlePointerDown方法中会把该触摸事件添加到手势竞争场, 并进行手势竞争.
void _handlePointerDown(PointerDownEvent event) {
// 添加event到手势竞技场
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
// GestureRecognizer#addPointer
void addPointer(PointerDownEvent event) {
// GestureRecognizer 负责将自身添加到全局指针路由器, 以接收此指针的后续事件,
// 并将Pointer添加到GestureArenaManager, 以跟踪该指针。
addAllowedPointer(event);
//...
}
// 最终, 重写addAllowedPointer的控件会被添加到手势竞技场中.
GestureBinding.instance!.gestureArena.add(event.pointer, this),
手势冲突
我们通过Down事件进一步收集发现, 并不是所有的Widget都会响应触摸事件, 而是通过RenderPointerListener
进行不同的触摸事件识别并回调到RawGestureDetector
.
如果在页面中存在多个RawGestureDetector
就会有触摸事件冲突问题, 具体由哪个Widget接受触摸事件? 每一个RawGestureDetector都响应了触摸事件怎么处理? 这就需要了解手势的竞争留存问题.
TODO 手势竞技场