如何在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
相关产品推荐
相关产品推荐

