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

如何在Flutter中使用Canvas或其他方法绘制适配分辨率的图片图形?

Flutter 实现任意分辨率图片上的多类型图形绘制与分辨率适配

核心思路

后端返回的图片分辨率不固定,核心是将后端提供的原始坐标转换为当前图片显示区域的适配坐标,再通过CustomPaint在图片上层叠加绘制各类图形。关键在于计算「原始图片尺寸」与「显示区域尺寸」的缩放比例,以此完成所有图形坐标的映射转换。

实现步骤

1. 获取图片尺寸与计算缩放比例

先加载网络图片并获取原始宽高,再结合显示区域尺寸计算缩放比例:

// 加载网络图片并获取原始尺寸
Future<Size> getImageSize(String imageUrl) async {
  final image = await NetworkImage(imageUrl).resolve(const ImageConfiguration());
  return Size(image.width.toDouble(), image.height.toDouble());
}

// 计算缩放比例(保持图片宽高比)
double scaleX = displaySize.width / originalImageSize.width;
double scaleY = displaySize.height / originalImageSize.height;
double scale = min(scaleX, scaleY);

2. 封装图形绘制工具类

创建工具类统一处理不同图形的绘制逻辑,接收适配后的坐标与画笔参数:

class ShapePainter {
  // 绘制矩形
  static void drawRect(Canvas canvas, List<Offset> points, Paint paint) {
    final rect = Rect.fromPoints(points[0], points[2]);
    canvas.drawRect(rect, paint);
  }

  // 绘制圆形(以第一个点为圆心,第二个点到圆心的距离为半径)
  static void drawCircle(Canvas canvas, List<Offset> points, Paint paint) {
    final center = points[0];
    final radius = (points[1] - center).distance;
    canvas.drawCircle(center, radius, paint);
  }

  // 绘制三角形
  static void drawTriangle(Canvas canvas, List<Offset> points, Paint paint) {
    final path = Path()
      ..moveTo(points[0].dx, points[0].dy)
      ..lineTo(points[1].dx, points[1].dy)
      ..lineTo(points[2].dx, points[2].dy)
      ..close();
    canvas.drawPath(path, paint);
  }

  // 绘制5角星形
  static void drawStar(Canvas canvas, Offset center, double radius, Paint paint) {
    final path = Path();
    double angle = pi / 5;
    for (int i = 0; i < 10; i++) {
      double r = i % 2 == 0 ? radius : radius / 2;
      double x = center.dx + r * cos(i * angle);
      double y = center.dy + r * sin(i * angle);
      i == 0 ? path.moveTo(x, y) : path.lineTo(x, y);
    }
    path.close();
    canvas.drawPath(path, paint);
  }
}

3. 使用CustomPaint实现图片叠加绘制

通过Stack将图片与CustomPaint叠加,在自定义CustomPainter中完成坐标转换与图形绘制:

class ImageWithShapes extends StatefulWidget {
  final String imageUrl;
  final List<Map<String, dynamic>> rawPoints;
  final Size displaySize;

  const ImageWithShapes({
    super.key,
    required this.imageUrl,
    required this.rawPoints,
    required this.displaySize,
  });

  @override
  State<ImageWithShapes> createState() => _ImageWithShapesState();
}

class _ImageWithShapesState extends State<ImageWithShapes> {
  Size? originalImageSize;
  double scale = 1.0;

  @override
  void initState() {
    super.initState();
    _loadImageSize();
  }

  void _loadImageSize() async {
    final size = await getImageSize(widget.imageUrl);
    setState(() {
      originalImageSize = size;
      double scaleX = widget.displaySize.width / size.width;
      double scaleY = widget.displaySize.height / size.height;
      scale = min(scaleX, scaleY);
    });
  }

  @override
  Widget build(BuildContext context) {
    if (originalImageSize == null) {
      return const Center(child: CircularProgressIndicator());
    }

    // 将原始坐标转换为适配坐标
    List<Offset> adaptedPoints = widget.rawPoints.map((point) {
      return Offset(point['x'] * scale, point['y'] * scale);
    }).toList();

    return Stack(
      children: [
        // 按比例显示图片
        Image.network(
          widget.imageUrl,
          width: widget.displaySize.width,
          height: widget.displaySize.height,
          fit: BoxFit.contain,
        ),
        // 叠加绘制图形
        CustomPaint(
          size: widget.displaySize,
          painter: ShapeOverlayPainter(
            adaptedPoints: adaptedPoints,
            shapeType: 'rect', // 可根据业务切换图形类型
          ),
        ),
      ],
    );
  }
}

// 自定义绘制器
class ShapeOverlayPainter extends CustomPainter {
  final List<Offset> adaptedPoints;
  final String shapeType;

  ShapeOverlayPainter({
    required this.adaptedPoints,
    required this.shapeType,
  });

  final Paint _paint = Paint()
    ..color = Colors.red.withOpacity(0.5)
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2;

  @override
  void paint(Canvas canvas, Size size) {
    switch (shapeType) {
      case 'rect':
        ShapePainter.drawRect(canvas, adaptedPoints, _paint);
        break;
      case 'circle':
        ShapePainter.drawCircle(canvas, adaptedPoints, _paint);
        break;
      case 'triangle':
        ShapePainter.drawTriangle(canvas, adaptedPoints, _paint);
        break;
      case 'star':
        final center = adaptedPoints[0];
        final radius = (adaptedPoints[1] - center).distance;
        ShapePainter.drawStar(canvas, center, radius, _paint);
        break;
    }
  }

  @override
  bool shouldRepaint(covariant ShapeOverlayPainter oldDelegate) {
    return oldDelegate.adaptedPoints != adaptedPoints || oldDelegate.shapeType != shapeType;
  }
}

4. 处理不同显示模式

  • 固定显示尺寸:传入固定displaySize(如Size(300, 400)),图片按比例适配到该尺寸,图形坐标同步缩放。
  • 实际尺寸展示:将displaySize设为原始图片尺寸,或通过MediaQuery获取屏幕尺寸,让图片全屏/按实际大小展示,缩放比例自动调整为1或屏幕适配比例。

关键注意点

  • 保持图片宽高比:使用BoxFit.contain避免图片拉伸,确保图形位置准确。
  • 跨平台兼容:代码基于Flutter原生API,无需额外修改即可在iOS、Android、Web等平台运行。
  • 动态更新:后端图形数据变化时,只需更新rawPoints触发重绘即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 17:56:06