Flutterで描画している画面をどうにかしてプログラムからキャプチャーできないかなー?と思ってFlutterのドキュメントを読み漁っていたら、なんかそれっぽいクラスを見つけました。
SceneはFlutterで描画される各フレームを表していて、SceneBuilderはSceneを組み立てるためのクラスらしいです。
じゃあFlutterの中でこれらを使ってる箇所はどこ?と思ってFlutterのコードを調べてみると……ありました。view.dartの中です。
import 'dart:ui' as ui;
//...中略
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
ui.window.render(scene);
ui.window.render
が実際の画面への描画を行う処理っぽいです。
ところで、Scene
クラスにはtoImageというメソッドがあります。これを使えばScene
の内容を画像として取得できそうな気がします。
上記のコードはRenderView
というクラスのメソッドで定義されています。じゃあRenderView
ってどっから取ってくればいいの?と思って読み進めていくと……なんとRendererBinding.instance.renderView
でアクセスできそうです。
ということは、こんな感じの処理をすればスクリーンショットが撮影できるのではないか…??
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// ByteDataを指定したパスのファイルに保存する。
/// Thanks to https://stackoverflow.com/questions/50119676/how-to-write-a-bytedata-instance-to-a-file-in-dart
Future<void> writeToFile(ByteData data, String path) {
final buffer = data.buffer;
return File(path).writeAsBytes(buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
}
Future<void> screenCapture(String path) async {
var builder = ui.SceneBuilder();
var scene = RendererBinding.instance.renderView.layer.buildScene(builder);
var image = await scene.toImage(ui.window.physicalSize.width.toInt(),
ui.window.physicalSize.height.toInt());
scene.dispose();
var data = await image.toByteData(format: ui.ImageByteFormat.png);
await writeToFile(data, path);
}
実際にやってみた
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(MyApp());
Future<void> writeToFile(ByteData data, String path) {
final buffer = data.buffer;
return File(path)
.writeAsBytes(buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
}
void capture({@required String path}) async {
var builder = ui.SceneBuilder();
var scene = RendererBinding.instance.renderView.layer.buildScene(builder);
var image = await scene.toImage(ui.window.physicalSize.width.toInt(),
ui.window.physicalSize.height.toInt());
scene.dispose();
var data = await image.toByteData(format: ui.ImageByteFormat.png);
await writeToFile(data, path);
}
class HomePage extends StatefulWidget {
@override
HomePageState createState() {
return HomePageState();
}
}
class HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Draw test'),
),
body: Center(
child: RaisedButton(
onPressed: () {
capture(path: '/sdcard/image.png');
},
child: Text('Hello world'),
),
),
);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
※外部ストレージ(/sdcard/
以下)にファイルを書き出すために、AndroidManifest.xmlに<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
を追加しました。Runtime Permissionを処理するのが面倒だったので、build.gradleのtargetSdkVersion
を21にしました。
実行した結果がこちらです。(アプリを実行してHello worldボタンをタップして、adb pull /sdcard/image.png
で端末のファイルを抜き出す)
Hello worldボタンを押した時点でのフレームが描画されていますね!