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

Flutter中如何用CustomPainter绘制定位图标替代绿色点?

解决方法:将绿色点替换为定位图标

要把点击画布添加的绿色点换成定位图标,你需要修改MyCustomPainter里的绘制逻辑,替换原来的drawPoints方法。下面提供两种可行方案:


方案一:纯Canvas绘制定位图标(无需图片资源)

直接用Canvas绘制定位图标的形状(圆形+三角形),无需依赖外部图片,性能更优:

修改MyCustomPainter类

class MyCustomPainter extends CustomPainter {
  final List<Offset> marks;
  const MyCustomPainter(this.marks);

  @override
  void paint(Canvas canvas, Size size) {
    // 定义图标尺寸
    final double iconSize = 30;
    // 填充色(绿色)
    final Paint fillPaint = Paint()..color = Colors.green;
    // 描边色(白色边框)
    final Paint strokePaint = Paint()
      ..color = Colors.white
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    // 遍历所有标记点,逐个绘制定位图标
    for (final offset in marks) {
      // 绘制圆形主体
      canvas.drawCircle(offset, iconSize / 2, fillPaint);
      canvas.drawCircle(offset, iconSize / 2, strokePaint);

      // 绘制下方的三角形
      final Path trianglePath = Path()
        ..moveTo(offset.dx, offset.dy + iconSize / 2)
        ..lineTo(offset.dx - iconSize / 2, offset.dy + iconSize)
        ..lineTo(offset.dx + iconSize / 2, offset.dy + iconSize)
        ..close();
      canvas.drawPath(trianglePath, fillPaint);
      canvas.drawPath(trianglePath, strokePaint);
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) {
    // 只有当标记列表变化时才重绘
    return !listEquals(oldDelegate.marks, marks);
  }
}

注意:原代码中paint方法的async关键字要去掉,CustomPainter的paint方法不支持异步操作。


方案二:使用图片资源(系统图标/本地Asset)

如果你想用现成的定位图标图片(比如Flutter内置的Icons.location_on或自定义Asset图片),可以通过加载图片并绘制到Canvas实现:

步骤1:准备图片资源

  • 若使用本地Asset图片:在pubspec.yaml中配置资源路径,比如:
    assets:
      - assets/location_icon.png
    

步骤2:修改MyCustomPainter类

class MyCustomPainter extends CustomPainter {
  final List<Offset> marks;
  final ImageProvider imageProvider;
  Image? _iconImage;
  bool _isImageLoaded = false;

  MyCustomPainter(this.marks, this.imageProvider) {
    // 预加载图片,加载完成后触发重绘
    imageProvider.resolve(const ImageConfiguration()).addListener(
      ImageStreamListener((info, _) {
        _iconImage = info.image;
        _isImageLoaded = true;
        markNeedsPaint();
      }),
    );
  }

  @override
  void paint(Canvas canvas, Size size) {
    if (!_isImageLoaded || _iconImage == null) return;

    final double iconSize = 30;
    for (final offset in marks) {
      // 以点击位置为中心绘制图片
      final Rect destRect = Rect.fromCenter(
        center: offset,
        width: iconSize,
        height: iconSize,
      );
      // 绘制图片到Canvas
      canvas.drawImageRect(
        _iconImage!,
        Rect.fromLTWH(0, 0, _iconImage!.width.toDouble(), _iconImage!.height.toDouble()),
        destRect,
        Paint(),
      );
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) {
    return !listEquals(oldDelegate.marks, marks) ||
           oldDelegate.imageProvider != imageProvider ||
           oldDelegate._isImageLoaded != _isImageLoaded;
  }
}

步骤3:更新HomePage中的CustomPaint调用

// ... 其他代码不变 ...
child: CustomPaint(
  // 传入AssetImage作为图标资源
  painter: MyCustomPainter(mark, const AssetImage('assets/location_icon.png')),
),
// ... 其他代码不变 ...

完整修改后的代码(方案一示例)

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // 引入listEquals

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<Offset> mark = [];

  void updatePosition(TapDownDetails details) {
    setState(() {
      mark.add(details.localPosition);
      print("mark:$mark");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onTapDown: updatePosition,
          child: RepaintBoundary(
              child: Container(
            margin: const EdgeInsets.only(left: 8, right: 8),
            height: 300,
            width: double.infinity,
            decoration: BoxDecoration(
                border: Border.all(color: Colors.blue),
                image: const DecorationImage(
                    image: AssetImage(
                      'assets/dark.jpg',
                    ),
                    fit: BoxFit.cover)),
            child: CustomPaint(
              painter: MyCustomPainter(mark),
            ),
          )),
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  final List<Offset> marks;
  const MyCustomPainter(this.marks);

  @override
  void paint(Canvas canvas, Size size) {
    final double iconSize = 30;
    final Paint fillPaint = Paint()..color = Colors.green;
    final Paint strokePaint = Paint()
      ..color = Colors.white
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    for (final offset in marks) {
      canvas.drawCircle(offset, iconSize / 2, fillPaint);
      canvas.drawCircle(offset, iconSize / 2, strokePaint);

      final Path trianglePath = Path()
        ..moveTo(offset.dx, offset.dy + iconSize / 2)
        ..lineTo(offset.dx - iconSize / 2, offset.dy + iconSize)
        ..lineTo(offset.dx + iconSize / 2, offset.dy + iconSize)
        ..close();
      canvas.drawPath(trianglePath, fillPaint);
      canvas.drawPath(trianglePath, strokePaint);
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) {
    return !listEquals(oldDelegate.marks, marks);
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 21:41:11