You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中CustomPainter路径与Tap点击位置不匹配问题求助

问题:点击CustomPainter绘制的Path无法触发命中检测

尝试点击CustomPainter生成的Path元素时,onTapDown返回的位置始终不在Path的范围内。已查看路径边界(Rect.fromLTRB(167.3, 142.5, 213.5, 177.4))和点击位置(如256.3333282470703, 527.0),尝试使用全局位置也无法解决问题。

原代码如下:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  Offset _tapPosition = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Custom Paint Example'),
        ),
        body: GestureDetector(
          onTapDown: (TapDownDetails details) {
            print('Tap location: ${details.localPosition}');
            print('Tap location: ${details.globalPosition}');
            final RenderBox renderBox = context.findRenderObject() as RenderBox;
            final Offset localOffset = renderBox.localToGlobal(details.globalPosition);
            Offset customPaintPosition = renderBox.localToGlobal(Offset.zero);
            print(Offset.zero);

            if (isTapInsidePath(LinePainter.path_1, details.localPosition)) {
              print('Tapped inside path_1 at ${details.localPosition}');
            }
          },
          child: Container(
            decoration: BoxDecoration(
              border: Border.all(
                color: Colors.black,
                width: 10.0,
              ),
            ),
            child: CustomPaintExample(),
          ),
        ),
      ),
    );
  }
  bool isTapInsidePath(Path path, Offset tapPosition) {
    print (path.getBounds());
    print (path.computeMetrics());

    return path.contains(tapPosition);
  }
}

class CustomPaintExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomPaint(
        painter: LinePainter(),
        size: Size(300, 300),
      ),
    );
  }
}

class LinePainter extends CustomPainter {
   static final Path path_1 = Path()
    ..moveTo(167.309,177.39)
    ..cubicTo(180.907,163.368,200.541,152.36499999999998,213.487,149.762)
    ..cubicTo(170.764,142.488,175.079,155.056,167.309,177.39)
    ..close();

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.blue
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;

    List<Offset> points = [
      Offset(50, 50),
      Offset(150, 100),
      Offset(100, 200),
      Offset(200, 250),
    ];

    for (int i = 0; i < points.length - 1; i++) {
      canvas.drawLine(points[i], points[i + 1], paint);
    }

    Paint paint_1_fill = Paint()..style=PaintingStyle.fill;
    paint_1_fill.color = Color(0xff000000).withOpacity(1.0);
    canvas.drawPath(path_1,paint_1_fill);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
解决方案

问题核心在于坐标上下文不匹配:

  1. 原GestureDetector的context属于MyApp,获取的RenderBox不是CustomPaint所在的容器,导致坐标转换错误
  2. CustomPaint被Center包裹,其本地坐标与GestureDetector的坐标存在偏移

修改步骤:

  1. 将GestureDetector移至CustomPaintExample内部,确保捕获的点击坐标对应CustomPaint的父容器
  2. 转换点击坐标到CustomPaint的本地空间,与Path的坐标体系对齐

修改后的完整代码:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Custom Paint Example')),
        body: Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.black, width: 10.0),
          ),
          child: const CustomPaintExample(),
        ),
      ),
    );
  }
}

class CustomPaintExample extends StatelessWidget {
  const CustomPaintExample({super.key});

  bool isTapInsidePath(Path path, Offset tapPosition) {
    print('Path bounds: ${path.getBounds()}');
    return path.contains(tapPosition);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTapDown: (TapDownDetails details) {
          final RenderBox customPaintRenderBox = context.findRenderObject() as RenderBox;
          // 将全局点击坐标转换为CustomPaint的本地坐标
          final Offset customPaintLocalPos = customPaintRenderBox.globalToLocal(details.globalPosition);
          print('CustomPaint local position: $customPaintLocalPos');

          if (isTapInsidePath(LinePainter.path_1, customPaintLocalPos)) {
            print('Tapped inside path_1 at $customPaintLocalPos');
          }
        },
        child: const CustomPaint(
          painter: LinePainter(),
          size: Size(300, 300),
        ),
      ),
    );
  }
}

class LinePainter extends CustomPainter {
  static final Path path_1 = Path()
    ..moveTo(167.309, 177.39)
    ..cubicTo(180.907, 163.368, 200.541, 152.365, 213.487, 149.762)
    ..cubicTo(170.764, 142.488, 175.079, 155.056, 167.309, 177.39)
    ..close();

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制蓝色线条
    final Paint linePaint = Paint()
      ..color = Colors.blue
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    final List<Offset> points = [
      const Offset(50, 50),
      const Offset(150, 100),
      const Offset(100, 200),
      const Offset(200, 250),
    ];
    for (int i = 0; i < points.length - 1; i++) {
      canvas.drawLine(points[i], points[i + 1], linePaint);
    }

    // 绘制黑色填充路径
    final pathPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black.withOpacity(1.0);
    canvas.drawPath(path_1, pathPaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

关键修改说明:

  • 将GestureDetector直接包裹CustomPaint,确保点击坐标的上下文与CustomPaint一致
  • 使用globalToLocal将全局坐标转换为CustomPaint的本地坐标,解决Center导致的偏移问题
  • 移除原MyApp中错误的坐标转换逻辑,避免上下文不匹配

内容的提问来源于stack exchange,提问作者Sidd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 17:20:19