Flutter技术问询:如何在图片上绘图并获取新图?如何绘制自由路径?
Hey there! I’ve got you covered on both your requirements—let’s walk through how to implement freehand drawing (like those red lines you mentioned) on an image and then save the final edited image.
1. Implementing Freehand Drawing on an Image
To draw free paths over an image, we’ll use GestureDetector to capture touch inputs and CustomPainter to render both the image and your drawn paths. Here’s a complete, ready-to-use example:
Step 1: Create a Stateful Widget to Track Drawing Paths
We’ll maintain the list of touch points and the target image in the widget state to update the drawing in real time:
import 'package:flutter/material.dart'; import 'dart:ui' as ui; import 'dart:typed_data'; class ImageDrawingWidget extends StatefulWidget { final ui.Image image; const ImageDrawingWidget({super.key, required this.image}); @override State<ImageDrawingWidget> createState() => _ImageDrawingWidgetState(); } class _ImageDrawingWidgetState extends State<ImageDrawingWidget> { final List<Offset> _drawPoints = []; final GlobalKey _repaintKey = GlobalKey(); // For capturing the final image @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: GestureDetector( // Capture initial touch down onPanDown: (details) { setState(() { final RenderBox renderBox = context.findRenderObject() as RenderBox; _drawPoints.add(renderBox.globalToLocal(details.globalPosition)); }); }, // Capture touch movement to draw continuous lines onPanUpdate: (details) { setState(() { final RenderBox renderBox = context.findRenderObject() as RenderBox; _drawPoints.add(renderBox.globalToLocal(details.globalPosition)); }); }, // Add a marker to end the current path (optional, for multi-segment drawing) onPanEnd: (details) => setState(() => _drawPoints.add(Offset.infinite)), child: RepaintBoundary( key: _repaintKey, child: CustomPaint( size: Size(widget.image.width.toDouble(), widget.image.height.toDouble()), painter: _ImagePainter(image: widget.image, drawPoints: _drawPoints), ), ), ), ), ElevatedButton( onPressed: () async => await _saveEditedImage(), child: const Text("Save Drawn Image"), ) ], ); } }
Step 2: Create the CustomPainter to Render Image and Paths
This painter will first draw the base image, then render your freehand path with the style you want (like thick red lines):
class _ImagePainter extends CustomPainter { final ui.Image image; final List<Offset> drawPoints; _ImagePainter({required this.image, required this.drawPoints}); @override void paint(Canvas canvas, Size size) { // Draw the base image first canvas.drawImage(image, Offset.zero, Paint()); // Draw the freehand path if (drawPoints.isNotEmpty) { final paint = Paint() ..color = Colors.red ..strokeWidth = 4.0 ..strokeCap = StrokeCap.round // Makes line ends smooth ..style = PaintingStyle.stroke; for (int i = 0; i < drawPoints.length - 1; i++) { // Skip the infinite offset markers that split paths if (drawPoints[i] != Offset.infinite && drawPoints[i+1] != Offset.infinite) { canvas.drawLine(drawPoints[i], drawPoints[i+1], paint); } } } } @override bool shouldRepaint(covariant _ImagePainter oldDelegate) { // Only repaint if the drawing points or image change return oldDelegate.drawPoints != drawPoints || oldDelegate.image != image; } }
2. Saving the Drawn Image as a New File/Image Object
To get the final edited image, we’ll use RepaintBoundary to capture the rendered widget, convert it to an image, and then process it. Add this method to your state class:
Future<void> _saveEditedImage() async { // Capture the rendered widget as an image final RenderRepaintBoundary boundary = _repaintKey.currentContext!.findRenderObject() as RenderRepaintBoundary; final ui.Image editedImage = await boundary.toImage(pixelRatio: 3.0); // Higher ratio = sharper output // Convert the image to bytes (PNG format) final ByteData? byteData = await editedImage.toByteData(format: ui.ImageByteFormat.png); final Uint8List? imageBytes = byteData?.buffer.asUint8List(); if (imageBytes != null) { // Here you can save the bytes to a file (using path_provider to get a local path) // Or pass the bytes to another widget, upload to a server, etc. print("Edited image captured successfully! Byte count: ${imageBytes.length}"); } }
Quick Tips to Customize
- Adjust
strokeWidth,color, andstrokeCapin thePaintobject to match your exact desired line style. - If loading the base image from assets or network, decode it to
ui.Imagefirst: userootBundle.load()+ui.decodeImageFromList()for assets, orNetworkImagewithresolve()for remote images.
内容的提问来源于stack exchange,提问作者said bendrioue

