遇到一个需求,需要用 Flutter 生成图片,最终实现的效果如下:
基本思路
使用 Canvas 绘制图片中各元素,然后使用 PictureRecorder 进行记录生成。
添加依赖
qr_flutter: ^3.1.0 image_gallery_saver: ^1.2.2 fluttertoast: ^4.0.0
实现代码
import 'dart:ui' as ui; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_platforms/generator/qrcode_generator.dart'; class ImageGenerator { generate(ui.Image topImg, ui.Image bottomImg, double screenWidth, String title, String content, String time) async { print("screenWidth = $screenWidth"); final recorder = ui.PictureRecorder(); ui.Paint paint = new Paint() ..isAntiAlias = true ..filterQuality = ui.FilterQuality.high; double rectTextTop = 150; // 文本显示矩形顶部距离图片最顶部的距离 double textMargin = 20; // 文字间间距,包括距离矩形边框左右间距 double pagePadding = 22; // 页面内容左右边距 double bottomHeight = 160; // 底部区域高度 // 获取标题高度等信息 double textMaxWidth = screenWidth - pagePadding * 2 - textMargin * 2; TextPainter titlePainter = new TextPainter( text: TextSpan( text: title, style: TextStyle( fontSize: 20, color: Colors.black87, fontWeight: FontWeight.bold, height: 1.2), ), textDirection: TextDirection.ltr) ..layout(maxWidth: textMaxWidth); var titleHeight = titlePainter.height; print("titleHeight = $titleHeight"); TextPainter contentPainter = new TextPainter( text: TextSpan( text: content, style: TextStyle( fontSize: 16, color: Colors.black87, fontWeight: FontWeight.normal, height: 1.5), ), textDirection: TextDirection.ltr) ..layout(maxWidth: textMaxWidth); var contentHeight = contentPainter.height; print("contentheight = $contentHeight"); double textHeight = titleHeight + contentHeight + 3 * textMargin; double bottom = textHeight + rectTextTop + textMargin * 2 + bottomHeight; double shadowBottom = textHeight + rectTextTop; print("bottom = $bottom"); if (bottom < 300) { bottom = 300; } // 利用矩形左边的X坐标、矩形顶部的Y坐标、矩形右边的X坐标、矩形底部的Y坐标确定矩形的大小和位置 var canvasRect = Rect.fromLTWH(0, 0, screenWidth, bottom); final canvas = Canvas(recorder, canvasRect); // 0. 绘制背景 canvas.drawColor(Color(0xfffefefe), BlendMode.color); // 1. 绘制图片 canvas.drawImageRect( topImg, Rect.fromLTWH(0, 0, topImg.width.toDouble(), topImg.height.toDouble()), Rect.fromLTWH( 0, 0, screenWidth, topImg.height * screenWidth / topImg.width), paint); // 2. 绘制时间 new TextPainter( text: TextSpan( text: time, style: TextStyle( fontSize: 16, color: Colors.white, fontWeight: FontWeight.normal, height: 1.5), ), textDirection: TextDirection.ltr) ..layout(maxWidth: textMaxWidth) ..paint(canvas, Offset(pagePadding, rectTextTop - 40)); // 2. 绘制矩形,先绘制矩形,否则文字被覆盖 paint.color = Color(0x00ffffffff); var rrect = RRect.fromRectAndRadius( Rect.fromLTWH(pagePadding, rectTextTop, screenWidth - pagePadding * 2, textHeight), Radius.circular(6)); var path = Path() ..moveTo(pagePadding, rectTextTop) ..lineTo(screenWidth - pagePadding, rectTextTop) ..lineTo(screenWidth - pagePadding, shadowBottom) ..lineTo(pagePadding, shadowBottom) ..close(); canvas.drawShadow(path, Colors.black, 6, true); canvas.drawRRect(rrect, paint); // 3. 绘制文字 titlePainter.paint( canvas, Offset(pagePadding + textMargin, rectTextTop + textMargin)); contentPainter.paint( canvas, Offset(pagePadding + textMargin, rectTextTop + textMargin * 2 + titleHeight)); double bottomTextWidth = screenWidth * 2 / 5; // 底部文案宽度 double bottomTextTopMargin = bottomHeight * 2 / 5; // 底部文案距离上面文字间距 canvas.drawImageRect( bottomImg, Rect.fromLTWH( 0, 0, bottomImg.width.toDouble(), bottomImg.height.toDouble()), // height / width = h / sc Rect.fromLTWH( screenWidth * 2 / 5, shadowBottom + bottomTextTopMargin + 5, bottomTextWidth, bottomImg.height.toDouble() * bottomTextWidth / bottomImg.width.toDouble()), paint); // 绘制二维码 new QrCodeGenerator(data: "123456", version: 2).drawQrCode( canvas, new Size(90, 90), 45, shadowBottom + bottomTextTopMargin); // 转换成图片 final picture = recorder.endRecording(); ui.Image img = await picture.toImage(screenWidth.toInt(), bottom.toInt()); print('img的尺寸: $img'); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData; } }
import 'package:flutter/material.dart'; import 'package:flutter_platforms/generator/paint_cache.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'dart:ui' as ui; // default color for the qr code pixels const _qrDefaultColor = Color(0xff111111); const _finderPatternLimit = 7; class QrCodeGenerator { ui.Image topImage; ui.Image bottomImage; /// The QR code version. final int version; // the qr code version /// The error correction level of the QR code. final int errorCorrectionLevel; // the qr code error correction level /// The color of the squares. final Color color; // the color of the dark squares /// The color of the non-squares (background). @Deprecated( 'You should us the background color value of your container widget') final Color emptyColor; // the other color /// If set to false, the painter will leave a 1px gap between each of the /// squares. final bool gapless; /// The image data to embed (as an overlay) in the QR code. The image will /// be added to the center of the QR code. ui.Image embeddedImage; /// Styling options for the image overlay. final QrEmbeddedImageStyle embeddedImageStyle; /// The base QR code data QrCode _qr; /// This is the version (after calculating) that we will use if the user has /// requested the 'auto' version. int _calcVersion; /// The size of the 'gap' between the pixels final double _gapSize = 0.25; /// Cache for all of the [Paint] objects. final _paintCache = PaintCache(); QrCodeGenerator( {@required String data, @required this.version, this.errorCorrectionLevel = QrErrorCorrectLevel.L, this.color = _qrDefaultColor, this.emptyColor, this.gapless = false, this.embeddedImage, this.embeddedImageStyle}) { _init(data); } bool _hasAdjacentVerticalPixel(int x, int y, int moduleCount) { if (y + 1 >= moduleCount) return false; return _qr.isDark(y + 1, x); } bool _hasAdjacentHorizontalPixel(int x, int y, int moduleCount) { if (x + 1 >= moduleCount) return false; return _qr.isDark(y, x + 1); } Size _scaledAspectSize( Size widgetSize, Size originalSize, Size requestedSize) { if (requestedSize != null && !requestedSize.isEmpty) { return requestedSize; } else if (requestedSize != null && _hasOneNonZeroSide(requestedSize)) { final maxSide = requestedSize.longestSide; final ratio = maxSide / originalSize.longestSide; return Size(ratio * originalSize.width, ratio * originalSize.height); } else { final maxSide = 0.25 * widgetSize.shortestSide; final ratio = maxSide / originalSize.longestSide; return Size(ratio * originalSize.width, ratio * originalSize.height); } } bool _isFinderPatternPosition(int x, int y) { final isTopLeft = (y < _finderPatternLimit && x < _finderPatternLimit); final isBottomLeft = (y < _finderPatternLimit && (x >= _qr.moduleCount - _finderPatternLimit)); final isTopRight = (y >= _qr.moduleCount - _finderPatternLimit && (x < _finderPatternLimit)); return isTopLeft || isBottomLeft || isTopRight; } bool _hasOneNonZeroSide(Size size) => size.longestSide > 0; void _drawFinderPatternItem( FinderPatternPosition position, Canvas canvas, _PaintMetrics metrics, ) { final totalGap = (_finderPatternLimit - 1) * metrics.gapSize; final radius = ((_finderPatternLimit * metrics.pixelSize) + totalGap) - metrics.pixelSize; final strokeAdjust = (metrics.pixelSize / 2.0); final edgePos = (metrics.inset + metrics.innerContentSize) - (radius + strokeAdjust); Offset offset; if (position == FinderPatternPosition.topLeft) { offset = Offset(metrics.inset + strokeAdjust, metrics.inset + strokeAdjust); } else if (position == FinderPatternPosition.bottomLeft) { offset = Offset(metrics.inset + strokeAdjust, edgePos); } else { offset = Offset(edgePos, metrics.inset + strokeAdjust); } // configure the paints final outerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternOuter, position: position); outerPaint.strokeWidth = metrics.pixelSize; outerPaint.color = color; final innerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternInner, position: position); innerPaint.strokeWidth = metrics.pixelSize; innerPaint.color = emptyColor ?? Color(0x00ffffff); final dotPaint = _paintCache.firstPaint(QrCodeElement.finderPatternDot, position: position); dotPaint.color = color; final outerRect = Rect.fromLTWH(offset.dx, offset.dy, radius, radius); canvas.drawRect(outerRect, outerPaint); final innerRadius = radius - (2 * metrics.pixelSize); final innerRect = Rect.fromLTWH(offset.dx + metrics.pixelSize, offset.dy + metrics.pixelSize, innerRadius, innerRadius); canvas.drawRect(innerRect, innerPaint); final gap = metrics.pixelSize * 2; final dotSize = radius - gap - (2 * strokeAdjust); final dotRect = Rect.fromLTWH(offset.dx + metrics.pixelSize + strokeAdjust, offset.dy + metrics.pixelSize + strokeAdjust, dotSize, dotSize); canvas.drawRect(dotRect, dotPaint); } void _drawImageOverlay( Canvas canvas, Offset position, Size size, QrEmbeddedImageStyle style) { final paint = Paint() ..isAntiAlias = true ..filterQuality = FilterQuality.high; if (style != null) { if (style.color != null) { paint.colorFilter = ColorFilter.mode(style.color, BlendMode.srcATop); } } final srcSize = Size(embeddedImage.width.toDouble(), embeddedImage.height.toDouble()); final src = Alignment.center.inscribe(srcSize, Offset.zero & srcSize); final dst = Alignment.center.inscribe(size, position & size); canvas.drawImageRect(embeddedImage, src, dst, paint); } void _init(String data) { if (!QrVersions.isSupportedVersion(version)) { throw QrUnsupportedVersionException(version); } // configure and make the QR code data final validationResult = QrValidator.validate( data: data, version: version, errorCorrectionLevel: errorCorrectionLevel, ); if (!validationResult.isValid) { throw validationResult.error; } _qr = validationResult.qrCode; _calcVersion = _qr.typeNumber; _initPaints(); } void _initPaints() { // Cache the pixel paint object. For now there is only one but we might // expand it to multiple later (e.g.: different colours). _paintCache.cache( Paint()..style = PaintingStyle.fill, QrCodeElement.codePixel); // Cache the empty pixel paint object. Empty color is deprecated and will go // away. _paintCache.cache( Paint()..style = PaintingStyle.fill, QrCodeElement.codePixelEmpty); // Cache the finder pattern painters. We'll keep one for each one in case // we want to provide customization options later. for (final position in FinderPatternPosition.values) { _paintCache.cache(Paint()..style = PaintingStyle.stroke, QrCodeElement.finderPatternOuter, position: position); _paintCache.cache(Paint()..style = PaintingStyle.stroke, QrCodeElement.finderPatternInner, position: position); _paintCache.cache( Paint()..style = PaintingStyle.fill, QrCodeElement.finderPatternDot, position: position); } } /// 绘制二维码 drawQrCode(Canvas canvas, Size size, double dx, double dy) async { canvas.save(); canvas.translate(dx, dy); // if the widget has a zero size side then we cannot continue painting. if (size.shortestSide == 0) { print("[QR] WARN: width or height is zero. You should set a 'size' value " "or nest this painter in a Widget that defines a non-zero size"); return; } final paintMetrics = _PaintMetrics( containerSize: size.shortestSide, moduleCount: _qr.moduleCount, gapSize: (gapless ? 0 : _gapSize), ); // draw the finder pattern elements _drawFinderPatternItem(FinderPatternPosition.topLeft, canvas, paintMetrics); _drawFinderPatternItem( FinderPatternPosition.bottomLeft, canvas, paintMetrics); _drawFinderPatternItem( FinderPatternPosition.topRight, canvas, paintMetrics); double left; double top; final gap = !gapless ? _gapSize : 0; // get the painters for the pixel information final pixelPaint = _paintCache.firstPaint(QrCodeElement.codePixel); pixelPaint.color = color; Paint emptyPixelPaint; if (emptyColor != null) { emptyPixelPaint = _paintCache.firstPaint(QrCodeElement.codePixelEmpty); emptyPixelPaint.color = emptyColor; } for (var x = 0; x < _qr.moduleCount; x++) { for (var y = 0; y < _qr.moduleCount; y++) { // draw the finder patterns independently if (_isFinderPatternPosition(x, y)) continue; final paint = _qr.isDark(y, x) ? pixelPaint : emptyPixelPaint; if (paint == null) continue; // paint a pixel left = paintMetrics.inset + (x * (paintMetrics.pixelSize + gap)); top = paintMetrics.inset + (y * (paintMetrics.pixelSize + gap)); var pixelHTweak = 0.0; var pixelVTweak = 0.0; if (gapless && _hasAdjacentHorizontalPixel(x, y, _qr.moduleCount)) { pixelHTweak = 0.5; } if (gapless && _hasAdjacentVerticalPixel(x, y, _qr.moduleCount)) { pixelVTweak = 0.5; } final squareRect = Rect.fromLTWH( left, top, paintMetrics.pixelSize + pixelHTweak, paintMetrics.pixelSize + pixelVTweak, ); canvas.drawRect(squareRect, paint); } } if (embeddedImage != null) { final originalSize = Size( embeddedImage.width.toDouble(), embeddedImage.height.toDouble(), ); final requestedSize = embeddedImageStyle != null ? embeddedImageStyle.size : null; final imageSize = _scaledAspectSize(size, originalSize, requestedSize); final position = Offset( (size.width - imageSize.width) / 2.0, (size.height - imageSize.height) / 2.0, ); // draw the image overlay. _drawImageOverlay(canvas, position, imageSize, embeddedImageStyle); } canvas.restore(); } } class _PaintMetrics { _PaintMetrics( {@required this.containerSize, @required this.gapSize, @required this.moduleCount}) { _calculateMetrics(); } final int moduleCount; final double containerSize; final double gapSize; double _pixelSize; double get pixelSize => _pixelSize; double _innerContentSize; double get innerContentSize => _innerContentSize; double _inset; double get inset => _inset; void _calculateMetrics() { final gapTotal = (moduleCount - 1) * gapSize; var pixelSize = (containerSize - gapTotal) / moduleCount; _pixelSize = (pixelSize * 2).roundToDouble() / 2; _innerContentSize = (_pixelSize * moduleCount) + gapTotal; _inset = (containerSize - _innerContentSize) / 2; } }
import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:flutter_platforms/generator/image_generator.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; class ImageGeneratorPage extends StatefulWidget { @override _ImageGeneratorPageState createState() => _ImageGeneratorPageState(); } class _ImageGeneratorPageState extends State{ ByteData _imgBytes; ui.Image _topImage; ui.Image _bottomImage; @override void initState() { super.initState(); _loadImage('images/icon2.jpg').then((image) { setState(() { _topImage = image; }); }); _loadImage('images/bottom_text.png').then((image) { setState(() { _bottomImage = image; }); }); } @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; return Scaffold( backgroundColor: Colors.teal, body: Center( child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(12.0), child: RaisedButton( child: Text("Image generate"), onPressed: () { _generate(screenWidth); }, ), ), _imgBytes != null ? Container( child: Image.memory( Uint8List.view(_imgBytes.buffer), height: 500, )) : Container() ], ), ), ); } /// 加载图片 Future _loadImage(String path) async { var data = await rootBundle.load(path); var codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); var info = await codec.getNextFrame(); return info.image; } void _generate(double screenWidth) async { ByteData byteData = await ImageGenerator().generate( _topImage, _bottomImage, screenWidth, "90后海归硕士多次偷快递 压力太大只为看看里面是什么", "3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,1111111112222欧某问了她门牌号码并帮她找到了该住户的快递。但她离开后不久,此住户真正的物主来找快递未果,向欧某反映自己的快递丢失。欧某再次查找监控,11", "2019年7月1日 英山网"); saveFile(byteData); setState(() { _imgBytes = byteData; }); } saveFile(ByteData byteData) async { Uint8List pngBytes = byteData.buffer.asUint8List(); final result = await ImageGallerySaver.saveImage(pngBytes); //这个是核心的保存图片的插件 print("result = $result"); Fluttertoast.showToast( msg: "filePath = $result", toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: Colors.yellow, textColor: Colors.black, fontSize: 16.0); } }
以上就是Flutter 生成图片保存至相册的示例的详细内容,更多关于Flutter 生成图片保存至相册的资料请关注脚本之家其它相关文章!