Flutter 生成圖片保存至相冊的示例
遇到一個(gè)需求,需要用 Flutter 生成圖片,最終實(shí)現(xiàn)的效果如下:

基本思路
使用 Canvas 繪制圖片中各元素,然后使用 PictureRecorder 進(jìn)行記錄生成。
添加依賴
qr_flutter: ^3.1.0 image_gallery_saver: ^1.2.2 fluttertoast: ^4.0.0
實(shí)現(xiàn)代碼
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; // 頁面內(nèi)容左右邊距
double bottomHeight = 160; // 底部區(qū)域高度
// 獲取標(biāo)題高度等信息
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坐標(biāo)、矩形頂部的Y坐標(biāo)、矩形右邊的X坐標(biāo)、矩形底部的Y坐標(biāo)確定矩形的大小和位置
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. 繪制時(shí)間
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);
// 轉(zhuǎn)換成圖片
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<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歐某問了她門牌號碼并幫她找到了該住戶的快遞。但她離開后不久,此住戶真正的物主來找快遞未果,向歐某反映自己的快遞丟失。歐某再次查找監(jiān)控,11",
"2019年7月1日 英山網(wǎng)");
saveFile(byteData);
setState(() {
_imgBytes = byteData;
});
}
saveFile(ByteData byteData) async {
Uint8List pngBytes = byteData.buffer.asUint8List();
final result = await ImageGallerySaver.saveImage(pngBytes); //這個(gè)是核心的保存圖片的插件
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 生成圖片保存至相冊的示例的詳細(xì)內(nèi)容,更多關(guān)于Flutter 生成圖片保存至相冊的資料請關(guān)注腳本之家其它相關(guān)文章!
- Flutter 路由插件fluro的使用
- Flutter如何完成路由攔截,實(shí)現(xiàn)權(quán)限管理
- Flutter 用自定義轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)頁面切換
- Flutter開發(fā)中的路由參數(shù)處理
- Flutter app頁面路由以及路由攔截的實(shí)現(xiàn)
- Flutter 底部彈窗如何實(shí)現(xiàn)多項(xiàng)選擇
- Flutter 利用CustomScrollView實(shí)現(xiàn)滑動(dòng)效果
- 如何在Flutter中嵌套Android布局
- Flutter 底部彈窗ModelBottomSheet的使用示例
- Flutter 使用fluro的轉(zhuǎn)場動(dòng)畫進(jìn)行頁面切換
相關(guān)文章
Android實(shí)現(xiàn) Shape屬性gradient 漸變效果
這篇文章主要介紹了Android 實(shí)現(xiàn)Shape屬性gradient 漸變效果,gradient用以定義漸變色,可以定義兩色漸變和三色漸變,及漸變樣式,具體實(shí)現(xiàn)代碼感興趣的朋友跟隨小編一起看看吧2019-11-11
一個(gè)Activity中多個(gè)Fragment的切換
經(jīng)常會(huì)遇到在一個(gè)activity界面上布局多個(gè)fragment,但是如何從一個(gè)fragment跳轉(zhuǎn)到另一個(gè)fragment呢?本文主要對一個(gè)Activity中多個(gè)Fragment的切換進(jìn)行介紹,下面跟著小編一起來看下吧2017-01-01
Android應(yīng)用中實(shí)現(xiàn)手勢控制圖片縮放的完全攻略
這篇文章主要介紹了Android應(yīng)用中實(shí)現(xiàn)手勢控制圖片縮放的完全攻略,采用了Matrix矩陣的方法,實(shí)例講解了包括觸摸點(diǎn)設(shè)置與各種沖突的處理等方面,相當(dāng)全面,需要的朋友可以參考下2016-04-04
Android開發(fā)之手勢檢測及通過手勢實(shí)現(xiàn)翻頁功能的方法
這篇文章主要介紹了Android開發(fā)之手勢檢測及通過手勢實(shí)現(xiàn)翻頁功能的方法,結(jié)合實(shí)例形式分析了Android GestureDetector類實(shí)現(xiàn)手勢檢測功能的相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
Android彈出DatePickerDialog并獲取值的方法
這篇文章主要為大家詳細(xì)介紹了Android彈出DatePickerDialog并獲取值的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05
另外兩種Android沉浸式狀態(tài)欄實(shí)現(xiàn)思路
這篇文章主要為大家介紹了另外兩種Android沉浸式狀態(tài)欄實(shí)現(xiàn)思路,android5.0及以后版本都支持給狀態(tài)欄著色,而目前android主流版本還是4.4,想要深入了解的朋友可以參考一下2016-01-01

