Flutterで画面のスクリーンショットを撮る

Flutterで描画している画面をどうにかしてプログラムからキャプチャーできないかなー？と思ってFlutterのドキュメントを読み漁っていたら、なんかそれっぽいクラスを見つけました。

* [Scene class](https://docs.flutter.io/flutter/dart-ui/Scene-class.html)
* [SceneBuilder class](https://docs.flutter.io/flutter/dart-ui/SceneBuilder-class.html)

SceneはFlutterで描画される各フレームを表していて、SceneBuilderはSceneを組み立てるためのクラスらしいです。

じゃあFlutterの中でこれらを使ってる箇所はどこ？と思ってFlutterのコードを調べてみると……ありました。[view.dart](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/view.dart#L194-L198)の中です。

```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](https://docs.flutter.io/flutter/dart-ui/Scene/toImage.html)というメソッドがあります。これを使えば`Scene`の内容を画像として取得できそうな気がします。
上記のコードは`RenderView`というクラスのメソッドで定義されています。じゃあ`RenderView`ってどっから取ってくればいいの？と思って読み進めていくと……なんと`RendererBinding.instance.renderView`でアクセスできそうです。

ということは、こんな感じの処理をすればスクリーンショットが撮影できるのではないか…？？

```dart
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);
}
```

## 実際にやってみた

```dart:lib/main.dart
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ボタンを押した時点でのフレームが描画されていますね！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/56668/4fdbe030-28de-7f97-acd2-e3d383f231cd.png)