在flutter手势中常常会遇到给容器添加手势,但是点击位置不在子组件上会导致手势不响应,我们通常的解决方案是设置GestureDetector
的behavior
为HitTestBehavior.opaque
或者HitTestBehavior.translucent
先说一下HitTestBehavior
的三个枚举值
enum HitTestBehavior {
//只有子组件命中,自己也算命中
deferToChild,
//自己始终命中,不管子组件是否命中,但是会阻碍后面的组件命中
opaque,
//自己始终命中,不管子组件是否命中,不会阻碍后面的组件命中
translucent,
}
其实第一个好理解,但是第二个和第三个如果只是看注释,有点难理解,翻翻源码吧,我这里看的是GestureDetector
中renderObjectRenderPointerListener -> 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;
其实这个代码可以理解为两个结果,一个是是否将自己加入到响应者中
一个是是否返回true
,因为这里看到opaque
或者translucent
的一个相同的结果都是将自己加入到响应者中,不同点是一个函数返回true
,一个返回false
我们可以再看下Stack
的renderObjectRenderStack
中对hitTest
实现方案
// ------ RenderStack 父类 RenderBox的代码 ------
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return defaultHitTestChildren(result, position: position);
}
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;
}
关键的变量是isHit
,如果isHit
是true
,就退出while循环,不在继续遍历兄弟组件了,通过这个去理解上面说的两个结果.
translucent
会继续遍历兄弟组件(如果有的话),opaque
打断遍历,不再遍历兄弟组件
简单来说translucent
就是我可以响应事件,但是我不影响后面的兄弟节点也能响应事件的能力,但是这里说的响应的能力并不是一定会响应,所有的响应者里边最后会挑选最上边的一个响应,所以后边这句话需要理解一下.
而opaque
就直接了当了,我响应不响应事件,弟弟们都没权利来响应,因为压根就没有将兄弟节点加到响应者的列表里
写段代码来验证下
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Listener;
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(
children: <Widget>[
Positioned(
left: 100,
top: 100,
child: GestureDetector(
onTap: () => print('A'),
child: Container(
color: Colors.blue,
width: 100,
height: 100,
),
),
),
Positioned(
left: 150,
top: 150,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
// onTap: () => print('B'),
child: Container(
width: 100,
height: 100,
),
),
),
],
),
),
),
);
}
}
最上面的组件是透明的,如果设置成opaque
,但是没有实现onTap
,点击重叠部分,下面的组件的onTap
不会被调用,但是如果换成translucent
就会被调用,如果这个时候将透明组件的onTap
打开,则只会调用透明组件的onTap