HitTest源头
GestureBinding._handlePointerEventImmediately在收到触点信息时调用,进行hitTest的情况有三种:
- PointerDownEvent——该触点是按下事件(通过手指按下触发)
- PointerSignalEvent——该触点是鼠标滚轮事件(通过鼠标滚轮触发)
- PointerHoverEvent——该触点是悬浮事件(在电脑上用触摸板触发)
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
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;
}());
}
...
}
hitTest的一些类
HitTestable
抽象类
/// An object that can hit-test pointers.
abstract class HitTestable {
HitTestable._();
void hitTest(HitTestResult result, Offset position);
}
实现了HitTestable的类都具有hitTest(命中测试)的能力;
hitTestDispatcher
抽象类
/// An object that can dispatch events.
abstract class HitTestDispatcher {
HitTestDispatcher._();
void dispatchEvent(PointerEvent event, HitTestResult result);
}
实现了hitTestDispatcher的类都具有dispatchEvent(分发事件)的能力
HitTestTarget
抽象类
/// An object that can handle events.
abstract class HitTestTarget {
HitTestTarget._();
void handleEvent(PointerEvent event, HitTestEntry entry);
}
实现了HitTestTarget类都具有handleEvent(接受处理事件)的能力
HitTestEntry和HitTestResult
HitTestEntry是命中测试采集的数据,如果被命中,会讲被命中的目标用HitTestEntry包裹; HitTestResult是命中测试的结果,内部维护着变量_path,_path是一个数组,用来存储被命中的目标,内部元素是HitTestEntry,
实现了这些功能的类
GestureBinding中实现了HitTestable、hitTestDispatcher、HitTestTarget,因此它具备了命中测试、分发事件、处理事件的能力;
RendererBinding中使用on关键字继承了HitTestable,也具备了命中测试的能力
RendererBinding是渲染树和engine的胶水类,内部的renderView即是渲染树的根节点,hitTest的最开始也是从这里开始的
RenderObject实现了HitTestTarget,因此它具备了处理事件的能力;
RenderBox虽然没有实现HitTestable,但在该基类有hitTest方法,说明它一样具备命中测试的的能力。
Flutter中几乎所有的渲染实体都直接或间接继承自RenderBox,而RenderBox又集成自RenderObject
GestureBinding
GestureBinding是一个mixin class,它并不是一个实体类,所以一定有个类混入了它,
在程序启动入口runApp()我们可以找到答案,
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding
因此WidgetsFlutterBinding是它的主类;在通过_handlePointerEventImmediately方法开始命中测试时,会首先进入到RendererBinding.hitTest方法,为什么首先会进入这里呢,因为多个mixin类存在相同方法时,后面混入的会覆盖前面的,而RendererBinding混入的时机比GestureBinding晚。而在最后又通过super.hitTest(result, position);回调到GetureBinding.hitTest;
##RendererBinding.hitTest##
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
assert(result != null);
assert(position != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
##GetureBinding.hitTest##
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
HitTest传递链
上面说了,首先会进入到RendererBinding.hitTest方法内,然后开始调用renderView.hitTest(renderView是渲染树的最根节点),然后顺着渲染树的节点一直向下传递,在叶子节点开始返回结果,所以hitTest的执行顺序是从根节点到叶子节点,而得到的结果正好相反,叶子节点->根节点;
##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;
}
在最后会把GetureBinding也加入到命中测试列表
左边是一个Widget层级,右侧是对应的RenderObject
该图来源
生成的hitTest列表如下⬇️:
这里注意的是TextSpan并没有RenderObject,并且他也没有hitTest的能力,但是它是依托于RenderParagraph的,所以它会在
RenderParagraph.hitTestChildren被加入列表中;
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
// Hit test text spans.
bool hitText = false;
final TextPosition textPosition = _textPainter.getPositionForOffset(position);
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
if (span != null && span is HitTestTarget) {
result.add(HitTestEntry(span as HitTestTarget));
hitText = true;
}
...
}
这样就得到了一个由叶子节点到根节点的命中列表。
注意: 能加入到hitTest列表的节点不一定有hitTest能力,但是一定需要拥有handleEvent(处理事件)的能力
HitTest规则
在RenderBox基类中hitTest()方法的规则是:如果命中孩子或者命中自己即表示被命中,会加入到命中测试列表,返回true;然后它的hitTestChildren和hitTestSelf方法直接返回flase,所以子类可以实现这几个方法自定义命中规则;
下面看下RenderDecoratedBox:
RenderDecoratedBox
BoxDecoration对应的RenderObject是RenderDecoratedBox,它是一个装饰器,用来给对象添加圆角,背景图、颜色等等;
由于RenderDecoratedBox没有实现hitTest方法,所以会进入它的父类RenderBox.hitTest方法,随后进入RenderDecoratedBox.hitTestSelf;
RenderDecoratedBox.hitTestSelf是极其简单的,只是调用了_decoration.hitTest,它是直接返回true,然后命中成功,将自己添加到命中列表
@override
bool hitTestSelf(Offset position) {
return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
}
HitTestBehavior的作用
behavior表示命中测试过程中的表现策略。它是一个枚举,提供了三个值,分别是:
HitTestBehavior.deferToChild:默认模式,表示是否命中取决于它的child是否被命中HitTestBehavior.opaque:Listener的child可以被响应,可以被命中,会阻止事件穿透HitTestBehavior.translucent:Listener的child可以被响应,不可以被命中,会阻止事件穿透
@override
bool hitTest(BoxHitTestResult result, { required 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;