【交互 widget】Flutter Listener
大家好,我是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 都是方法名,逻辑可以看前面的源码分析。
转载自:https://juejin.cn/post/7173448283791032351