Flutter进阶:基于 hitTest 的像素点击事件响应控制研究

155 阅读2分钟

一直有个疑惑?

若是不规则图片,如何控制它的事件响应控制:如何控制哪些部分触发点击事件,哪些区域无响应。近期终于找到了解决办法,分享给大家。

实现核心是重写 RenderBox 子类中 hitTest 方法:

bool hitTest(BoxHitTestResult result, {required Offset position}) {

二、示例展示

simulator_screenshot_03CFC5CA-A3A6-4F9B-A99F-43D024065053.jpg

示例效果:黄色部分触发点击事件,点击蓝色线框内的白色区域不会触发点击事件。

1、使用示例

Container(
  decoration: BoxDecoration(
    color: Colors.transparent,
    border: Border.all(color: Colors.blue),
  ),
  child: MyCustomHitTestWidget(
    radius: 50,
    color: Colors.orange,
    onTap: () {
      DLog.d('Custom hit test area tapped!');
    },
    textSpan: TextSpan(
      text: '$runtimeType' * 1,
      style: TextStyle(
        color: Colors.yellow,
        fontSize: 14,
      ),
    ),
  ),
),

2、MyCustomHitTestWidget 源码

/// 自定义圆形组件支持 HitTest 自定义
class MyCustomHitTestWidget extends SingleChildRenderObjectWidget {
  MyCustomHitTestWidget({
    super.key,
    required this.radius,
    required this.color,
    this.onTap,
    this.textSpan,
    this.textPainter,
  });

  final double radius;
  final Color color;
  final VoidCallback? onTap;
  final TextSpan? textSpan;
  final TextPainter? textPainter;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MyHitTestRenderBox(
      radius: radius,
      color: color,
      onTap: onTap,
      textSpan: textSpan,
      textPainter: textPainter,
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant MyHitTestRenderBox renderObject) {
    renderObject
      ..radius = radius
      ..color = color
      ..onTap = onTap
      ..textSpan = textSpan
      ..textPainter = textPainter;
  }
}

class MyHitTestRenderBox extends RenderBox {
  MyHitTestRenderBox({
    required this.radius,
    required this.color,
    this.onTap,
    this.textSpan,
    this.textPainter,
  });

  double radius;
  Color color;
  VoidCallback? onTap;
  TextSpan? textSpan;
  TextPainter? textPainter;

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    final center = Offset(constraints.maxWidth / 2, constraints.maxHeight / 2);
    // final r = (position - center).distance <= radius;

    final rect = RRect.fromRectAndCorners(
      Rect.fromLTRB(0, 0, radius * 2, radius * 2),
      topLeft: Radius.circular(radius),
      topRight: Radius.circular(radius),
      bottomLeft: Radius.circular(radius),
      bottomRight: Radius.circular(radius),
    );

    final contains = rect.contains(position);
    if (contains) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    if (event is PointerDownEvent) {
      onTap?.call();
    }
  }

  @override
  void performLayout() {
    size = constraints.constrain(Size(radius * 2, radius * 2));
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final canvas = context.canvas;
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;

    // Draw the circular hit test area
    canvas.drawCircle(
      offset + Offset(radius, radius),
      radius,
      paint,
    );

    // Draw text at the center
    final textPainterNew = textPainter ??
        TextPainter(
          text: textSpan ??
              TextSpan(
                text: '$runtimeType',
                style: TextStyle(
                  color: Colors.red,
                  fontSize: 16,
                ),
              ),
          textDirection: TextDirection.ltr,
        );
    textPainterNew.layout(maxWidth: size.width);
    final textOffset = offset +
        Offset(
          (size.width - textPainterNew.width) / 2,
          (size.height - textPainterNew.height) / 2,
        );
    textPainterNew.paint(canvas, textOffset);
  }
}

3、hitTest 返回 true 会相应事件,返回 false 则不响应,这部分代码决定是否响应式事件

总结

1、事件分发流程:

当用户进行触摸操作时,Flutter 会通过以下流程分发事件:

  1. 触摸事件传播:当设备上发生触摸事件时,Flutter 的渲染层会从根节点开始,逐级传递触摸事件到子节点。
  2. HitTest 过程:在每个 RenderObject 上,Flutter 会通过 hitTest 方法来判断当前的触摸位置是否位于该组件的区域内。如果是,它会处理事件。
  3. 事件消费:如果某个组件的 hitTest 检测到触摸点并处理了这个事件,它就会“消费”该事件,其他组件将不再接收到该事件。

简而言之:

  • hitTest 是一个重要的机制,负责将触摸事件分发到对应的 Widget。
  • 它是 Flutter 渲染引擎处理触摸事件的核心部分,通过检查触摸位置来决定哪个组件接收到事件。
  • 在自定义 Widget 时,我们可以通过重写 hitTest 方法来实现自定义的触摸事件处理。

github