大家好,我是17,今天的每日 widget 为大家介绍 Listener。
Listener 调用回调以响应 pointer 事件。Listener 是底层的 pointer 事件处理,并不涉及到手势,所以不会有竞争的问题。
源码分析
Listener 自身的代码很简单,只是包了一个皮,点击测试的逻辑是它的父类完成的。
代码所在类 RenderProxyBoxWithHitTestBehavior
@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; 复制代码
hitTest 是 Listener 最重要的逻辑。 result 是测试结果列表,只有添加到 result 里才能响应 pointer 事件。
- 判断点击位置是否在 size 内,如果不在就返回 false,不会响应 pointer 事件。
- 点击位置如果在 size 内,child 的 点击测试和 自身的点击测试有一个通过即为通过。
- behavior == HitTestBehavior.translucent 无论怎样测试都通过,但返回值不变。
- behavior == HitTestBehavior.opaque 测试一定会通过。返回值一定为 true。
如果是多 child 的 widget,hitTest 的逻辑是怎样的呢?
代码所在类 RenderBoxContainerDefaultsMixin
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; } 复制代码
在兄弟节点间,前面的节点先绘制,后面的节点后绘制,所以后面的会覆盖前面的,被覆盖的节点是不应该响应点击的,所以从最后一个 child 开始判断,如果 hitTest 通过,也就不用判断前面的了。
使用 Listener
如果不涉及到手势,只是响应 pointer 事件,Listener 再合适不过。
const Listener({ super.key, this.onPointerDown, this.onPointerMove, this.onPointerUp, this.onPointerHover, this.onPointerCancel, this.onPointerPanZoomStart, this.onPointerPanZoomUpdate, this.onPointerPanZoomEnd, this.onPointerSignal, this.behavior = HitTestBehavior.deferToChild, super.child, }) 复制代码
虽然响应的事件很多,但其实用起来都一样,我们就拿 onPointerDown 举例吧。需要体验的是 behavior,前面源码中已经分析过,behavior 对 pointer 事件会产生影响。
behavior 的默认值 HitTestBehavior.deferToChild,deferTo 的英文含义是遵从,实际的行为也确实如此,child hitTest 通过,就能响应 pointer 事件,否则没有任何响应。
为了方便看效果,我们自定义一个类。
class MyHitTest extends SingleChildRenderObjectWidget { const MyHitTest({super.key, super.child}); @override RenderObject createRenderObject(BuildContext context) { return RenderMyHitTest(); } } class RenderMyHitTest extends RenderProxyBox { @override bool hitTest(BoxHitTestResult result, {required Offset position}) { return false; } } 复制代码
在 hitTest 中直接返回 false,表明 hitTest 失败。我们来测试下,看看 Listener 还能否响应 pointer 事件。
Listener( onPointerDown: ((event) { print(event); }), child: MyHitTest( child: Container( width: 100, height: 100, color: Colors.blue, ), )) 复制代码
现在无论怎么点击都泥牛入海。
我们修改下 hitTest,让他 返回 true。
bool hitTest(BoxHitTestResult result, {required Offset position}) { return true; } 复制代码
现在可以响应点击了。但是 MyHitTest 自身并没有加入到点击列表中,所以自身是不能响应 pointer 事件的。
我们做个实验来验证这一点,override handleEvent ,看看能否接收到 event。
class RenderMyHitTest extends RenderProxyBox { @override void handleEvent(PointerEvent event, HitTestEntry entry) { print(event); } @override bool hitTest(BoxHitTestResult result, {required Offset position}) { return true; } } 复制代码
结果无法接收到 event,虽然 hitTest 已经成功,但这仅仅是表明上层可以响应 pointer 事件,MyHitTest 自己是不能的。
为了能让 MyHitTest 也能响应 pointer 事件,把它加到列表中就好了。
@override bool hitTest(BoxHitTestResult result, {required Offset position}) { result.add(HitTestEntry(this)); return true; } 复制代码
关于 HitTestBehavior.deferToChild 的效果我们都测试完成了,下面看下 HitTestBehavior.opaque 的效果。因为代码不多,下面给出完整代码。
class MyHitTest extends SingleChildRenderObjectWidget { const MyHitTest({super.key, super.child}); @override RenderObject createRenderObject(BuildContext context) { return RenderMyHitTest(); } } class RenderMyHitTest extends RenderProxyBox { @override bool hitTest(BoxHitTestResult result, {required Offset position}) { return false; } } Listener( behavior: HitTestBehavior.opaque, onPointerDown: ((event) { print(event); }), child: MyHitTest( child: Container( width: 100, height: 100, color: Colors.blue, ), )); 复制代码
注意 hitTest 返回 false。 HitTestBehavior.opaque 的作用就是:hitTestSelf 一定成功,能响应 pointer 事件。
HitTestBehavior.translucent 也能让自身能响应 pointer 事件,但 hitTest 的结果取决于 hitTestChildren 与 hitTestSelf 的结果,这个结果会影响上层能否响应 pointer 事件。
hitTestSelf,hitTestChildren 都是方法名,逻辑可以看前面的源码分析。