场景 测试说这个左上角的返回按钮太难按了。
解决:1 给返回键增加padding,配合HitTestBehavior.translucent增大响应区域。
问题 增加padding,可能会影响到周围的组件的位置。与UI不符
期待的最佳解决方案:在不改变组件尺寸的情况下,增大响应区域。
觉得写的不好的可以看这篇文章 参考文章:juejin.cn/post/700203…
知识点:
1 在Stack组件中,通过Position摆放组件。不会影响Stack的大小
2 Flutter组件事件是从上往下,也就是父->子的方式传递。只有父组件接受了事件,子组件才有机会处理事件。
//关键代码
//判断自己是否命中
if (_size!.contains(position)) {
//校验children是否命中
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
所以解决思路是:1 通过在Stack中通过Position来增大响应区域。像这样
var defaultWidth = 100.0;
var defaultHeight = 100.0;
Stack(
children: [
SizedBox(
width: defaultWidth,
height: defaultHeight,
),
Positioned(child: Container(
width: defaultWidth * 2,
height: defaultHeight * 2,
color: Colors.yellow,
))
],
);
这样写 Stack 的size最终是 100 * 100,内部Container的尺寸200*200. 当前情况下,点击 100 * 100的区域 SizedBox 是可以响应事件的。但是点击 150 * 150 虽然点击的在Container范围之内,但是不在Stack范围内。也就是说Container的父节点没有机会处理事件,他的子节点也就没机会处理事件。
2 解决点击 150 * 150 Stack不能处理事件。 只需要重写 RenderStack(RenderStack 是Stack 对应的RenderObject)的hitTest。自己不进行是否包含坐标点的判断,直接交给子节点去判断。这样 200 * 200的Container 就有机会处理 100 * 100 以外的事件。
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
//自己直接返回true(RenderStack) 直接交给子节点 子节点命中就处理事件
if (contains(position)) {
if (hitTestChildren(result, position: position) ||
hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
// 永远为 true
bool contains(Offset position) => true;
我的封装代码 具体使用方式 参考上面的链接。
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class AddClickAreaGestureDetector extends StatelessWidget {
final double width;
final double height;
final Widget child;
final VoidCallback onTap;
final double wScale;
final double hScale;
const AddClickAreaGestureDetector({super.key,required this.width,
required this.height,required this.child,required this.onTap, this.wScale = 4,
this.hScale = 4
});
@override
Widget build(BuildContext context) {
var defaultWidth = 100.0;
var defaultHeight = 100.0;
Stack(
children: [
SizedBox(
width: defaultWidth,
height: defaultHeight,
),
Positioned(child: Container(
width: defaultWidth * 2,
height: defaultHeight * 2,
color: Colors.yellow,
))
],
);
var clickWidth = width * wScale;
var clickHeight = height * hScale;
return _AddClickAreaStack(
clipBehavior: Clip.none,
children: [
//设置占据位置
SizedBox(width: width,height: height),
Positioned(
left: - (clickWidth - width) / 2 ,
top: - (clickHeight - height) / 2,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: (){
debugPrint("======onTap===");
onTap.call();
},
child: Container(
alignment: Alignment.center,
width: clickWidth,
height: clickHeight,
child: child,
),
)
)
],
);
}
}
class _AddClickAreaStack extends Stack {
const _AddClickAreaStack(
{required super.children,super.clipBehavior,super.fit});
@override
_AddClickAreaRenderStack createRenderObject(BuildContext context) {
return _AddClickAreaRenderStack(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
clipBehavior: clipBehavior
);
}
}
class _AddClickAreaRenderStack extends RenderStack with RenderBoxHitTestWithoutSizeLimit {
_AddClickAreaRenderStack({
required super.alignment,
super.textDirection,
required super.fit,
super.clipBehavior
});
}
//不校验自己是否命中的Column
class ColumnHitTestWithoutSizeLimit extends Column
with FlexHitTestWithoutSizeLimitmixin{
ColumnHitTestWithoutSizeLimit({
super.key,
super.mainAxisAlignment,
super.mainAxisSize,
super.crossAxisAlignment,
super.textDirection,
super.verticalDirection,
super.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
super.children,
});
}
//不校验自己是否命中的Row
class RowHitTestWithoutSizeLimit extends Row
with FlexHitTestWithoutSizeLimitmixin {
RowHitTestWithoutSizeLimit({
super.key,
super.mainAxisAlignment,
super.mainAxisSize,
super.crossAxisAlignment,
super.textDirection,
super.verticalDirection,
super.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
super.children,
});
}
mixin FlexHitTestWithoutSizeLimitmixin on Flex {
@override
RenderFlex createRenderObject(BuildContext context) {
return RenderFlexHitTestWithoutSizeLimit(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context),
verticalDirection: verticalDirection,
textBaseline: textBaseline,
clipBehavior: clipBehavior,
);
}
}
class RenderFlexHitTestWithoutSizeLimit extends RenderFlex
with
RenderBoxHitTestWithoutSizeLimit , RenderBoxChildrenHitTestWithoutSizeLimit{
RenderFlexHitTestWithoutSizeLimit({
super.children,
super.direction,
super.mainAxisSize,
super.mainAxisAlignment,
super.crossAxisAlignment,
super.textDirection,
super.verticalDirection,
super.textBaseline,
super.clipBehavior,
});
}
mixin RenderBoxHitTestWithoutSizeLimit on RenderBox {
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
assert(() {
if (!hasSize) {
if (debugNeedsLayout) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Cannot hit test a render box that has never been laid out.'),
describeForError(
'The hitTest() method was called on this RenderBox'),
ErrorDescription(
"Unfortunately, this object's geometry is not known at this time, "
'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested.'),
ErrorHint('If you are trying '
'to perform a hit test during the layout phase itself, make sure '
"you only hit test nodes that have completed layout (e.g. the node's "
'children, after their layout() method has been called).'),
]);
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot hit test a render box with no size.'),
describeForError('The hitTest() method was called on this RenderBox'),
ErrorDescription(
'Although this node is not marked as needing layout, '
'its size is not set.'),
ErrorHint('A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.'),
]);
}
return true;
}());
if (contains(position)) {
if (hitTestChildren(result, position: position) ||
hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
// 永远为 true
bool contains(Offset position) => true;
// size.contains(position);
}
mixin RenderBoxChildrenHitTestWithoutSizeLimit on RenderFlex{
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return hitTestChildrenWithoutSizeLimit(
result,
position: position,
children: getChildrenAsList().reversed,
);
}
bool hitTestChildrenWithoutSizeLimit(
BoxHitTestResult result, {
required Offset position,
required Iterable<RenderBox> children,
}) {
final List<RenderBox> normal = <RenderBox>[];
for (final RenderBox child in children) {
if ((child is RenderBoxHitTestWithoutSizeLimit) &&
childIsHit(result, child, position: position)) {
return true;
} else {
normal.insert(0, child);
}
}
for (final RenderBox child in normal) {
if (childIsHit(result, child, position: position)) {
return true;
}
}
return false;
}
bool childIsHit(BoxHitTestResult result, RenderBox child,
{required Offset position}) {
final ContainerParentDataMixin<RenderBox> childParentData =
child.parentData as ContainerParentDataMixin<RenderBox>;
final Offset offset = (childParentData as BoxParentData).offset;
final bool isHit = result.addWithPaintOffset(
offset: offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - offset);
return child.hitTest(result, position: transformed);
},
);
return isHit;
}
}