-
Flutter事件处理
-
事件预处理
-
应用事件都是操作系统从硬件中断收集然后传递给应用程序,在flutter中事件也是由Window类通过
onPointerDataPacket
来接收硬件传递过来的事件,也就是说该方法是flutter
接收事件的源头,而该方法是在类GestureBinding
初始化的时候设置的。所以在类GestureBinding
的方法_handlePointerDataPacket
中开始处理事件。
针对手势的处理流程可以参考下图
在GestureBinding
的源码中可以看到具体的实现,它主要做了以下三步操作。
1.接收事件
由于接收硬件的数据都是与真实设备相关的,所以需要通过PointerEventConverter
的expand
方法来隔离设备相关性。类似Android
中的px
转dp
。
只有将数据进行设备隔离后才能放入FIFO的双端队列——ListQueue
中。笔者认为这里使用队列是为了防止Widget
处理不及时从而导致阻塞。如下图所示。
2.Down事件处理
与Android
一样,Down
事件在flutter
中也是非常重要的一个事件,它是从ListQueue
中取出的第一个事件。通过该事件,flutter
可以获取到目标Widget
到根Widget
的路径,并将路径上的所有Widget
添加到一个集合List
中。如下图所示。
注意: 添加到集合中的Widget
必须都要继承自RenderObjectWidget
。
将路径上的所有Widget
添加到集合后,就会遍历该集合,并将Down
事件交给集合中的所有Widget
处理。如下图所示。
上图中Widget
的HandlerEvent
函数基本上都是空实现,但Listener
例外。所以基本上都是通过Listener
来监听手势事件。
3.其他事件处理
在Down
事件处理完毕以后,其他事件(如move
、up
等)会遍历集合,并将事件传递给集合中的所有Widget
处理,如下图所示。
上图中Widget
的HandlerEvent
函数基本上都是空实现,但Listener
例外。所以基本上都是通过Listener
来监听手势事件。
可以发现事件是从目标Widget
往RenderView
(根Widget
)传递的。如果是做前端开发的,想必对这一流程比较熟悉,因为这与前端开发中浏览器的事件冒泡机制相似, 但在flutter
中是没有机制来取消或停止”冒泡“过程的,而浏览器的冒泡是可以停止的。
-
flutter事件响应
在事件响应之前,需要对命中对象进行测试,并确认是由谁来响应,这就是hitTest的过程。在hitTest前会创建HitTestResult
对象并向HitTestResult
添加HitTestEntry
命中对象最后保存到_hitTests
表中;
hitTests命中测试
执行GestureBinding
的hitTest
会先经过RenderBinding
去执行hitTests
方法,首先RenderView
根节点判断是否存在子节点并判断子节点是否在命中范围内。
/flutter/packages/flutter/lib/src/rendering/binding.dart:RendererBinding->hitTest
@override
void hitTest(HitTestResult result, Offset position) {
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
1-RenderView:hitTest
/flutter/packages/flutter/lib/src/rendering/view.dart:RenderView -> hitTest
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null) { // 子布局是否命中
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
result.add(HitTestEntry(this));
return true;
}
2-RenderBox:hitTest
RenderView
根节点的子节点是RenderSemanticsAnnotations
根本上是继承自RenderBox
。默认情况下RenderBox
的hitTestSelf
和hitTestChildren
返回都是false
,具体命中逻辑由继承者自行重写实现(hitTestChildren
是具备有多子节点布局来实现默认情况下false
表示无子节点)。
/flutter/packages/flutter/lib/src/rendering/box.dart:RenderBox -> hitTest
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;
}
@protected
bool hitTestSelf(Offset position) => false;
@protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
若命中了自身hitTestSelf
或子级hitTestChildren
时添加BoxHitTestEntry
(继承自HitTestEntry
);hitTestChildren
又是递归调用因此组件树命中测试是深度优先遍历,符合命中规则子节点会比父节点先被添加到****HitTestResult
****队列中例如RenderDecoratedBox
的hitTestSelf
重写方法通过Decoration
的hitTest
进行判断是否在命中范围内。
@override
bool hitTestSelf(Offset position) {
return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
}
-
flutter事件处理
-
dispatchEvent事件分发
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { if (hitTestResult == null) { try { pointerRouter.route(event); // 暂时忽略 } catch (exception, stack) { ... } return; } for (final HitTestEntry entry in hitTestResult.path) { try { entry.target.handleEvent(event.transformed(entry.transform), entry); } catch (exception, stack) { ... } } }
-
pointerRouter
接收事件类-
遍历
HitTestResult
的HitTestEntry
数组执行handleEvent
方法遍历顺序是从最底层子节点开始往上走,从最后的
HitTestEntry
可以看到分别是RenderView
和GestureBinding
两个类处理事件分发。命中测试符合要求节点记录下来后通过该方法进行遍历操作并执行每个节点
handleEvent
方;handleEvent
由每个节点重写实现具体逻辑。-
事件拦截
-
事件既然有分发,那么肯定也能够拦截,相对于Android
而言,flutter
的事件拦截要简单很多。在flutter
中,可以通过AbsorbPointer
与IgnorePointer
这两个Widget
来拦截事件。
AbsorbPointer
AbsorbPointer
是一个Widget
,它的主要作用就是拦截其子Widget
响应事件。它的实现原理其实很简单,就是在响应Down
事件时,不会将其子Widget
添加到集合中,这样其子Widget
就无法接收到事件。如下图所示。
在AbsorbPointer
中可以修改absorbing
的值来让子Widget
响应事件。
注意:AbsorbPointer本身可以响应事件
IgnorePointer
IgnorePointer
和AbsorbPointer
类似,不同的是IgnorePointer设置为不响应事件时(即ignoring = true),IgnorePointer
的child不响应事件,但是事件会传递到下一层;
在IgnorePointer
中可以修改ignoring
的值来让自己及其子Widget
响应事件。
注意:IgnorePointer本身无法响应事件
-
自定义widget和事件响应
flutter有“处处都是widget”的概念,对widget的自定义通常可以组合widget来完成UI绘制,但有时候我们也需要通过通过CustomPaint自绘定制控件。简单widget自绘制可以使用通过RenderObject自绘,复杂的控flutter为自绘制控件提供了ContainerRenderObjectMixin 和 RenderBoxContainerDefaultsMixin 两个 mixin,这两个 mixin 实现了通用的绘制和事件处理相关逻辑。
渲染节点数据
在组合多个结点完成自绘制时存在父节点和叶节点的树形结构,在 Flutter 的布局系统中,该类负责存储父节点所需要的子节点的布局信息,当然该信息偶尔也会用于子节点的布局。
每个 RenderObject 类中都有 parentData 这样一个成员,该成员只能通过 setupParentData 方法赋值,RenderObject 的子类可以通过重写该方法将 ParentData 的子类赋值给 parentdata,以扩展 ParentData 的功能:
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
接下来看一下该类的 Hierarchy 结构:
ParentData 类继承结构
先无视用于滑动的 Sliver 相关的类和用于表格布局的 TabelCellParentData,我们来分析一下剩余的 ParentData类的作用。
ParentData
class ParentData {
/// Called when the RenderObject is removed from the tree.
@protected
@mustCallSuper
void detach() { }
@override
String toString() => '<none>';
}
这是所有 ParentData 的基类,没有存储任何信息也没有实现功能,只定义了一个空实现的 detach()
方法,该方法会在 RenderObject 被移出 tree 的时候调用,这给子类提供了一个在 RenderObject 移出时更新信息的时机。
BoxParentData
/// Parent data used by [RenderBox] and its subclasses.
class BoxParentData extends ParentData {
/// The offset at which to paint the child in the parent's coordinate system.
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
该类注释写的很明确,用于 RenderBox 和它的子类,只有一个 offset 属性,该属性用于存储 child 的布局信息,也就是 child 应该被摆在哪个位置,通常在 child 大小确定后,parent 负责根据自身逻辑将 child 的位置赋值到这里。
ContainerBoxParentData
查看源码后发现该类是个空类,只是为了方便子类混入 ContainerParentDataMixin。
ContainerParentDataMixin
该类使用频率很高,基本上所有父节点的 ParentData 都混入了该类,该类需要与ContainerRenderObjectMixin 共同使用,主要解决了对 child 的管理,它用双链表存储了所有子节点并提供了方便的接口去获取他们。对于开发者,一般来说只用到 ContainerRenderObjectMixin 中的 firstChild
、lastChild
、childCount
,用来获取首末 child,child的个数,配合使用 ContainerParentDataMixin 中的 previousSibling
、nextSibling
就可以对 child 进行遍历了。
这些 ParentData 的基类解决了 child 的布局位置信息的存储和 child 的管理以及引用的获取,再往下的子类就是与各布局的功能相关的类了,如 FlexParentData,存储了 flex 和 fit 的值,分别表示该 child 的 flex 比重和 布局的 fit 策略。
渲染节点布局和事件响应
RenderBoxContainerDefaultsMixin
为了简化多节点渲染widget的开发,flutter引入了ContainerRenderObjectMixin和RenderBoxContainerDefaultsMixin。
ContainerRenderObjectMixin主要负责了节点树的管理,主要绘制和事件处理都在RenderBoxContainerDefaultsMixin进行处理。其中
/// Returns the baseline of the first child with a baseline.
///
/// Useful when the children are displayed vertically in the same order they
/// appear in the child list.
double? defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline) {
....
}
/// Returns the minimum baseline value among every child.
///
/// Useful when the vertical position of the children isn't determined by the
/// order in the child list.
double? defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline) {
...
}
defaultComputeDistanceToFirstActualBaseline和defaultComputeDistanceToHighestActualBaseline计算对齐元素
/// Paints each child by walking the child list forwards.
///
/// See also:
///
/// * [defaultHitTestChildren], which implements hit-testing of the children
/// in a manner appropriate for this painting strategy.
void defaultPaint(PaintingContext context, Offset offset) {
ChildType? child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData! as ParentDataType;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
defaultPaint
则执行每个节点的绘制工作。参数offset为布局计算器传递的布局偏移,节点自身可通过修改childParentData.offset
进行节点的偏移转换工作。
在事件处理方面,主要依赖是RenderBoxContainerDefaultsMixin中defaultHitTestChildren:
/// Performs a hit test on each child by walking the child list backwards.
///
/// Stops walking once after the first child reports that it contains the
/// given point. Returns whether any children contain the given point.
///
/// See also:
///
/// * [defaultPaint], which paints the children appropriate for this
/// hit-testing strategy.
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
ChildType? child = lastChild;
while (child != null) {
// The x, y parameters have the top left of the node's box as the origin.
final ParentDataType childParentData = child.parentData! as ParentDataType;
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;
}