【Flutter】HitTestBehavior想点哪里点哪里
点击事件响应
点击组件中的HitTestBehavior
属性支持三个值:opaque
、translucent
、deferToChild
。其在命中测试起到一定作用可改变原有命中逻辑从而是实现不同点击触发事件。
属性值
HitTestBehavior
在RenderProxyBoxWithHitTestBehavior
有具体实现应用场景。
/flutter/packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxWithHitTestBehavior
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position); // 若behavior 等于HitTestBehavior.opaque可命中
if (hitTarget || behavior == HitTestBehavior.translucent) { // 若behavior等于HitTestBehavior.translucent可命中
result.add(BoxHitTestEntry(this, position));
}
}
return hitTarget;
}
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
在检查子节点命中情况时判断是否opaque
,在检查自身命中时判断translucent
,因此可知HitTestBehavior.opaque
>HitTestBehavior.translucent
>HitTestBehavior.deferToChild
。
由于基础组件对于hitTest
和hitTestChildren
非false
因此存在将事件消费情况(例如SizedBox
无子节点情况下默认false
)。为了验证HitTestBehavior
可行性就需要自定义一个组件重写hitTest
和hitTestChildren
方法。这里以ColoredBox
为基础自定义组件重写_RenderColoredBox
的命中判定默认都为false
。
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
return false;
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return false;
}
在Stack
中设置两个重叠子节点:
- 默认情况下点击组件
behavior
是HitTestBehavior.deferToChild
根据点击组件子节点命中测试要求而定 - 当为
HitTestBehavior.deferToChild
情况下,点击事件由最上层子节点响应 - 当为
HitTestBehavior.translucent
情况下,点击事件都透传所有子节点都响应 - 当为
HitTestBehavior.deferToChild
情况下,点击事件无响应(因为自定义_ColoredBoxWithNoHitTest忽略了命中测试)
Stack(
alignment: Alignment.center,
children: [
Listener(
behavior: value,
onPointerDown: (down) {
showSnackBarMsg(context, 'onPointerDown -> Listener -> 外',
clear: false, duration: const Duration(milliseconds: 500));
},
child: _ColoredBoxWithNoHitTest(
color: Colors.red.withOpacity(0.5),
child: const SizedBox(
height: 200,
width: 250,
),
)),
Listener(
behavior: value,
onPointerDown: (down) {
showSnackBarMsg(context, 'onPointerDown -> Listener -> 内',
clear: false, duration: const Duration(milliseconds: 500));
},
child: _ColoredBoxWithNoHitTest(
color: Colors.red.withOpacity(0.5),
child: const SizedBox(
height: 100,
width: 150,
),
),
),
],
);
在使用
HitTestBehavior
也需要注意到所作用节点是否支持忽略命中,因为很多情况下有些组件默认情况下实现hitTest
是true
状态。
实战举例
一个布局实现如下:最外层Container
设置边框可视化点击区域,内部子节点是带有GestureDetector
的Container
无边框无背景色其内部子节点有Image
、Text
等。
Container(
decoration: buildBoxDecorationBorder(),
child: GestureDetector(
// behavior: HitTestBehavior.deferToChild, // 设置后点击空白区域无响应
behavior: HitTestBehavior.translucent, // 设置后点击空白区域有响应
onTap: () {
showSnackBarMsg(context, 'onTap -> GestureDetector -> Container');
},
child: Container(
alignment: Alignment.center,
height: 150,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/img_640_640.jpg', width: 50),
const Text(
"文本区域\n"
"文本区域\n"
"文本区域\n"
"文本区域\n",
style:
TextStyle(color: Colors.white, backgroundColor: Colors.red),
),
ColoredBox(
color: Colors.red.withOpacity(0.5),
child: const SizedBox(
width: 150,
height: 100,
child: Center(
child: Text(
"behavior是opaque或translucent\n点击空白区域才能响应",
style: TextStyle(color: Colors.white),
),
),
),
)
],
),
),
),
);
- 默认情况下点击子节点
Container
内部子组件可以响应点击事件 - 点击子节点
Container
内部空白区域无法响应点击事件 - 修改
GestureDetector
的behavior
为HitTestBehavior.translucent
点击边框内任务区域都能响应点击事件
分析缘由
Container
是复合组件由多种其他组件嵌套而成,例如配置color
会嵌套ColoredBox
,增加边框decoration
会嵌套DecoratedBox
。
属性
ColoredBox
内部实现_RenderColoredBox
,它的命中测试逻辑是由`RenderProxyBoxWithHitTestBehavior`判断。
/flutter/packages/flutter/lib/src/rendering/proxy_box.dart: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;
是否命中测试以`hitTestChildren`作为判断依据,因为默认情况下`behavior`是`deferToChild`。一般情况而言子节点命中测试都是`true`,所以有`Color`的`Container`是一般是命中测试的。
非也非也阅读_RenderColoredBox
源码可知默认情况下behavior
是HitTestBehavior.opaque
状态,因此触摸组件监听时可以直接通过命中测试。
/flutter/packages/flutter/lib/src/widgets/basic.dart:_RenderColoredBox
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
_RenderColoredBox({ required Color color })
: _color = color,
super(behavior: HitTestBehavior.opaque);
...
}
属性
DecoratedBox
内部实现了RenderDecoratedBox
其hitTestSelf
方法实现由BoxDecoration
接管,从内部实现看命中测试逻辑可知
/flutter/packages/flutter/lib/src/painting/box_decoration.dart:BoxDecoration
@override
bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
...
switch (shape) {
case BoxShape.rectangle: // 矩形求边框是否在范围内
if (borderRadius != null) {
final RRect bounds = borderRadius!.resolve(textDirection).toRRect(Offset.zero & size);
return bounds.contains(position);
}
return true;
case BoxShape.circle: // 圆形求半径是否在命中范围内
final Offset center = size.center(Offset.zero);
final double distance = (position - center).distance;
return distance <= math.min(size.width, size.height) / 2.0;
}
}
因此设置了边框的Container
只要在边框范围内命中测试都是true
小总结
如上所知当Container
没有设置Color
和DecoratedBox
属性时若要让Container
整体命中测试就必须为点击组件设置为translucent
或者opaque
,相反则只能响应Container
内部子节点事件。