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

如何在Flutter中实现自定义4×3地图的两点连线功能?

实现4×3地图点击方格绘制连接折线的方案

我有一个自定义的4×3地图,想要实现点击两个不同方格时显示折线连接它们的功能,目前还没找到可行方案。

自定义地图
点击后需要显示的效果

以下是具体实现建议和代码示例:

一、基于现有CustomPaint的自定义实现步骤

  • 定义方格数据模型:给每个方格分配唯一标识,同时记录方格的中心点坐标和点击边界,方便判断点击位置归属:

    class Square {
      final String id;
      final Offset center;
      final Rect bounds; // 用于判断点击是否在方格内
    
      Square({required this.id, required this.center, required this.bounds});
    }
    
  • 处理点击交互:在页面状态类中维护两个变量存储两次点击的方格中心点,通过GestureDetector监听地图区域的点击事件,匹配点击位置对应的方格,更新选中状态。

  • 传递数据并动态重绘:修改MyPainter让它接收点击点参数,在paint方法中根据两个点绘制折线;同时将shouldRepaint返回true,确保状态变化时触发重绘。

  • 优化折线路径:根据地图网格的布局,调整折线的拐点逻辑(比如先水平后垂直,或先垂直后水平),让路径贴合地图的方格排列。

二、推荐的第三方库

如果不想手动处理绘制逻辑,可参考这些工具:

  • flutter_map:支持自定义图层绘制路径,适合复杂地图交互场景,可快速实现节点间的连线逻辑。
  • graphview:针对网格图、节点图设计,能自动处理节点间的路径生成与绘制,支持自定义样式。

修改后的完整示例代码

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Offset? _firstTap;
  Offset? _secondTap;
  // 示例方格数据,需根据实际地图坐标补充完整
  final List<Square> _squares = [
    Square(
      id: '1',
      center: const Offset(165, 100),
      bounds: Rect.fromLTWH(130, 70, 70, 60),
    ),
    Square(
      id: '2',
      center: const Offset(490, 100),
      bounds: Rect.fromLTWH(455, 70, 70, 60),
    ),
    // 其他方格依次添加
  ];

  void _handleTap(TapDownDetails details) {
    final tapPosition = details.localPosition;
    // 匹配点击的方格
    final tappedSquare = _squares.firstWhere(
      (square) => square.bounds.contains(tapPosition),
      orElse: () => Square(id: '', center: Offset.zero, bounds: Rect.zero),
    );

    if (tappedSquare.id.isEmpty) return;

    setState(() {
      if (_firstTap == null) {
        _firstTap = tappedSquare.center;
      } else if (_secondTap == null && _firstTap != tappedSquare.center) {
        _secondTap = tappedSquare.center;
      } else {
        // 重置选择,重新开始
        _firstTap = tappedSquare.center;
        _secondTap = null;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTapDown: _handleTap,
        child: Stack(
          children: [
            Center(
              child: Image.asset(
                'assets/Sơ đồ mô phỏng.png',
                height: 300,
              ),
            ),
            CustomPaint(
              painter: MyPainter(firstPoint: _firstTap, secondPoint: _secondTap),
              size: MediaQuery.of(context).size,
            ),
            Positioned(
              top: 50,
              left: 10,
              child: IconButton(
                onPressed: () {},
                icon: const Icon(
                  Icons.map,
                  size: 35,
                ),
              ),
            ),
            Positioned(
              top: 100,
              left: 10,
              child: IconButton(
                onPressed: () {},
                icon: const Icon(
                  Icons.pattern_sharp,
                  size: 35,
                ),
              ),
            ),
            Positioned(
              top: 150,
              left: 10,
              child: IconButton(
                onPressed: () {},
                icon: const Icon(
                  Icons.location_on,
                  size: 35,
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

class Square {
  final String id;
  final Offset center;
  final Rect bounds;

  Square({required this.id, required this.center, required this.bounds});
}

class MyPainter extends CustomPainter {
  final Offset? firstPoint;
  final Offset? secondPoint;

  MyPainter({this.firstPoint, this.secondPoint});

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制原有地图线条
    const p1 = Offset(165, 100);
    const p2 = Offset(165, 135);
    final paint1 = Paint()
      ..color = Colors.black
      ..strokeWidth = 3;
    const p3 = Offset(165, 135);
    const p4 = Offset(490, 135);
    final paint2 = Paint()
      ..color = Colors.black
      ..strokeWidth = 3;
    const p5 = Offset(490, 100);
    const p6 = Offset(490, 135);
    final paint3 = Paint()
      ..color = Colors.black
      ..strokeWidth = 3;
    canvas.drawLine(p1, p2, paint1);
    canvas.drawLine(p3, p4, paint2);
    canvas.drawLine(p5, p6, paint3);

    // 绘制选中方格的连接折线
    if (firstPoint != null && secondPoint != null) {
      final connectPaint = Paint()
        ..color = Colors.red
        ..strokeWidth = 4
        ..style = PaintingStyle.stroke;

      // 构建折线路径(可根据地图布局调整拐点逻辑)
      final path = Path()
        ..moveTo(firstPoint!.dx, firstPoint!.dy)
        ..lineTo(secondPoint!.dx, firstPoint!.dy)
        ..lineTo(secondPoint!.dx, secondPoint!.dy);

      canvas.drawPath(path, connectPaint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return oldDelegate is MyPainter &&
        (oldDelegate.firstPoint != firstPoint || oldDelegate.secondPoint != secondPoint);
  }
}

内容的提问来源于stack exchange,提问作者Hưng Nguyễn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 04:30:09