我是靠谱客的博主 瘦瘦春天,这篇文章主要介绍Flutter 生成图片保存至相册的示例,现在分享给大家,希望可以做个参考。

遇到一个需求,需要用 Flutter 生成图片,最终实现的效果如下:

基本思路

使用 Canvas 绘制图片中各元素,然后使用 PictureRecorder 进行记录生成。

添加依赖

复制代码
1
2
3
qr_flutter: ^3.1.0 image_gallery_saver: ^1.2.2 fluttertoast: ^4.0.0

实现代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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; } }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
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; } }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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<ImageGeneratorPage> { 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: <Widget>[ 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<ui.Image> _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 生成图片保存至相册的资料请关注靠谱客其它相关文章!

最后

以上就是瘦瘦春天最近收集整理的关于Flutter 生成图片保存至相册的示例的全部内容,更多相关Flutter内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(119)

评论列表共有 0 条评论

立即
投稿
返回
顶部