概要
LINEの友達登録のQRコードや、○○payの送金先などアプリ内で作成したプロフィールや画像、ゲームのスコア、QRコードなどを共有したい場面が出てくると思います。
そういった場面で使用できる方法を紹介したいと思います。
Widgetから画像を生成するだけなら簡単で、「Widgetから画像を生成する」を見るとほぼほぼ実装できると思います。
実装
ここからFlutter での実装を説明していきます。
1. Widgetから画像を生成する
ウィジェットから画像を生成するには、画像として書き出したいウィジェットをRepaintBoundary
で囲み、RepaintBoundaryのKeyからRenderObject
を取得することで、画像を生成することができます。
final _globalKey = GlobalKey();
Widget build(BuildContext context) {
return RepaintBoundary(
key: _globalKey,
child: SafeArea(
child: ListView.builder(
itemCount: 40,
itemBuilder: _buildRow,
),
),
);
}
// RepaintBoundary の key を渡す
Future<Uint8List> convertWidgetToImage(GlobalKey widgetGlobalKey) async {
// RenderObjectを取得
RenderRepaintBoundary boundary = widgetGlobalKey.currentContext.findRenderObject();
// RenderObject を dart:ui の Image に変換する
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData.buffer.asUint8List();
}
上記のコードで下記のように画像を生成することができます。
画像に変換したものはImage.memory()
を使用することでWidgetとして表示することも可能です。
上の画像を見ていただきたいのですが、生成した画像の下の方を見ると「範囲内」の文字が切れているのがわかります。
この書き方では画面の描画範囲外に出ているWidgetを画像に含めることができません。
この後に画面の描画範囲外にあるWidgetも含めて画像を生成するコードを紹介します。
2. 画面の描画範囲外にあるWidgetも含め画像を生成する
ListViewなどを使用していると描画範囲外にWidgetがあることが多々あると思います。
それらのWidgetを含めて画像を生成するにはRepaintBoundary
をSingleChildScrollView
で囲ってあげる必要があります。
「Widgetから画像を生成する」のサンプルコードを変更すると下記のようになります。
final _globalKey = GlobalKey();
Widget build(BuildContext context) {
return SingleChildScrollView(
primary: true,
child: RepaintBoundary(
key: _globalKey,
child: SafeArea(
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: 40,
itemBuilder: _buildRow,
),
),
),
);
}
コードを変更して生成した画像は下記のようになります。
これで、画像を生成したい範囲内のWidgetをすべて含めることができました。
次は右下にロゴマークを追加したいと思います。
3. 右下にロゴを埋め込み画像を生成する
Widget の変更はありません、変更があるのは画像生成のコードです。
2つの画像から1つの画像を生成するので、Canvas を使用して画像を生成します。
Widget からui.Image を作るまでは同じです。
ここでは、PictureRecorder、Canvas、CustomPainterを使用します。
class ConvertWidgetToImage {
Future<Uint8List> execute(GlobalKey widgetGlobalKey) async {
RenderRepaintBoundary boundary = widgetGlobalKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
// Canvasを用意
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(recorder);
// assets の画像を読み込む
final logoImage = await loadLocalImage();
// Canvasで2つの画像を合成する
ImagePainter(image, logo: logoImage)..paint(canvas, Size.infinite);
// Canvas に描いたものを画像にする
final ui.Image renderImage = await recorder.endRecording().toImage(
image.width, image.height + logoImage.height,
);
ByteData byteData = renderImage.toByteData(format: ui.ImageByteFormat.png);
return byteData.buffer.asUint8List();
}
Future<ui.Image> loadLocalImage() async {
Completer<ImageInfo> completer = Completer();
/// ImageProvider をui.Imageに変換している
AssetImage('assets/logo.png').resolve(ImageConfiguration()).addListener(
(ImageStreamListener((ImageInfo info, bool _) => completer.complete(info))),
);
ImageInfo imageInfo = await completer.future;
return imageInfo.image;
}
}
class ImagePainter extends CustomPainter {
/// Widgetから生成した画像
final ui.Image image;
/// 下部に追加するロゴ
final ui.Image logo;
ImagePainter(
this.image, {
this.logo,
});
@override
void paint(Canvas canvas, Size size) {
// canvas の左上に画像にしたいWidgetを配置する
canvas.drawImage(image, Offset(0, 0), Paint());
if (logo != null) {
// canvas の右下にロゴを配置する
canvas.drawImage(
logo,
Offset(image.width.toDouble() - logo.width.toDouble(),
image.height.toDouble() - logo.height.toDouble()),
Paint());
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
上記のコードで右下にロゴを追加した画像が生成出来ました。
ロゴの位置はOffsetの値を変更することで位置を変えることができます。
あとがき
投稿の日を1日間違えていて焦って書いたので、殴り書きになってしまいましたが上記の方法でWidgetから画像の生成ができます。
特に難しいことをしないで画像が生成できて便利ですね。
ちなみにShareプラグイン等を使用して画像を共有するには一度ローカルに保存する必要があるので、Tempディレクトリにでも保存して共有するようにしてください。
あとがきも投稿してから後日に書いています。次事は間に合うように書きます