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

Flutter如何通过自定义Widget绘制线条并在用户点击时修改线条角度与坐标

问题根源分析

你遇到的两个问题核心原因如下:

  • 视图不更新:HomeWidget是无状态组件,线条坐标硬编码在paint方法内部,且MyPainter的shouldRepaint返回false,就算数据变更也不会触发重绘逻辑
  • 拼接线条不显示:没有维护全局的线条坐标集合,新增线条没有同步到绘制数据源,拖动角度的逻辑也没有绑定手势事件修改对应坐标值
具体实现方案

1. 改造为有状态组件维护全局数据

将HomeWidget改为StatefulWidget,统一管理所有线条坐标、选中状态等可变数据:

class HomeWidget extends StatefulWidget {
  @override
  State<HomeWidget> createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  // 存储所有线条,每段线条存两个端点坐标,拼接线条共享端点即可
  List<List<Offset>> lines = [
    [const Offset(50, 100), const Offset(150, 75)],
    [const Offset(150, 75), const Offset(250, 250)]
  ];
  // 当前选中的线条索引、端点索引
  int? selectedLineIdx;
  int? selectedPointIdx;
}

2. 添加手势检测逻辑

用GestureDetector包裹CustomPaint,处理点击、拖动事件,实时修改坐标后调用setState触发更新:

@override
Widget build(BuildContext context) {
  return Center(
    child: GestureDetector(
      // 按下时判定选中的端点/新增拼接线条
      onPanStart: (details) {
        final localPos = details.localPosition;
        const selectThreshold = 10; // 选中判定阈值,可随线宽调整
        // 先查找是否点击到现有端点
        for (int i = 0; i < lines.length; i++) {
          for (int j = 0; j < lines[i].length; j++) {
            if ((localPos - lines[i][j]).distance < selectThreshold) {
              selectedLineIdx = i;
              selectedPointIdx = j;
              return;
            }
          }
        }
        // 没选中现有端点则新增拼接线条
        Offset? nearestEnd;
        double minDis = double.infinity;
        for (var line in lines) {
          for (var point in line) {
            final dis = (localPos - point).distance;
            if (dis < minDis) {
              minDis = dis;
              nearestEnd = point;
            }
          }
        }
        if (nearestEnd != null && minDis < 15) {
          setState(() {
            lines.add([nearestEnd, localPos]);
            selectedLineIdx = lines.length - 1;
            selectedPointIdx = 1;
          });
        }
      },
      // 拖动时更新坐标,拼接端点同步联动
      onPanUpdate: (details) {
        if (selectedLineIdx != null && selectedPointIdx != null) {
          setState(() {
            final newPos = details.localPosition;
            final oldPos = lines[selectedLineIdx!][selectedPointIdx!];
            // 更新当前选中端点
            lines[selectedLineIdx!][selectedPointIdx!] = newPos;
            // 同步更新所有共享该端点的其他线条,实现拼接联动
            for (int i = 0; i < lines.length; i++) {
              for (int j = 0; j < lines[i].length; j++) {
                if (lines[i][j] == oldPos) {
                  lines[i][j] = newPos;
                }
              }
            }
          });
        }
      },
      onPanEnd: (details) {
        selectedLineIdx = null;
        selectedPointIdx = null;
      },
      child: CustomPaint(
        size: const Size(300, 300),
        painter: MyPainter(lines: lines),
      ),
    ),
  );
}

3. 改造CustomPainter支持动态重绘

将线条数据作为参数传入Painter,修改重绘判定逻辑:

class MyPainter extends CustomPainter {
  final List<List<Offset>> lines;
  final Paint _linePaint = Paint()
    ..color = Colors.black
    ..strokeWidth = 4
    ..strokeCap = StrokeCap.round;
  final Paint _pointPaint = Paint()..color = Colors.blueAccent;

  MyPainter({required this.lines});

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制所有线条
    for (var line in lines) {
      canvas.drawLine(line[0], line[1], _linePaint);
    }
    // 绘制端点方便点击选中,不需要可以删除
    for (var line in lines) {
      canvas.drawCircle(line[0], 6, _pointPaint);
      canvas.drawCircle(line[1], 6, _pointPaint);
    }
  }

  @override
  bool shouldRepaint(covariant MyPainter oldDelegate) {
    // 仅线条数据变更时重绘,优化性能
    return oldDelegate.lines != lines;
  }
}
优化提示

如果需要支持拖动整段线条调整角度,可以在手势判定逻辑中增加点到线段的距离计算,点击位置在线段范围内时,选中整段线条,拖动时同步更新两个端点的相对坐标即可。
如果怕坐标比对精度问题,可以给每个端点增加唯一ID标识,用ID判断是否为共享拼接端点,性能和准确性更高。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 11:51:00