LoginSignup
22
13

More than 3 years have passed since last update.

Androidのテストで利用できるスクリーンショット取得APIのまとめ

Last updated at Posted at 2020-12-14

はじめに

Android開発でUIテストを実装していると、画面の表示確認やVisual Regression Test等の用途でスクリーンショットを取りたい場面が出てきます。
しかし、スクリーンショットを取得できるAPIは複数あり、適したものを選択しないと思ったようなスクリーンショットが取れないことがあります。
本記事では、Androidのテストで使用できるスクリーンショットのAPIを紹介し、それらを使うと実際にどのようなスクリーンショットが取れるのかを見ていきたいと思います。

こちらは Mobility Technologies Advent Calendar 2020 の14日目の記事です。

スクリーンショットを取得できるAPI

スクリーンショットは以下のAPIを利用して取得することができます。

本記事では、Canvasを使ったViewの書き出し・PixelCopy・UiDevice#takeScreenshotの3つのAPIを中心に説明していきます。
まずは、3つのAPIの利用方法を確認します。

CanvasをつかったViewの書き出し

Canvasをつかったスクリーンショットの取得は次のような実装になります。
ここではActivityを引数にとる実装をしていますが、任意のViewのインスタンスを渡すこともできるので、画面内の一部のViewに限定したスクリーンショットを取得することが可能です。

fun capture(activity: Activity) {

    val view = activity.window.decorView.rootView

    // スクリーンショットを取得したいViewサイズにあわせたBitmapを作成する
    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)

    // Canvasを生成し、スクリーンショットを取得したいViewをBitmapに書き出す
    val canvas = Canvas(bitmap)
    view.draw(canvas)

    // Bitmap#compressでファイルに書き出し
}

UiDevice#takeScreenshot

UiDeviceはUiAutomatorというUIテストフレームワークのAPIです。

UiAutomatorをテストコードから使用できるようにするには、次のdependencyを追加します。


androidTestImplementation 'androidx.test.uiautomator:uiautomator: X.X.X'

また、UiAutomatorはInstrumentation Testのコードからのみ使用することができます。

スクリーンショットを取得するコードは次のとおりです。

fun capture() {

    val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    // 保存先のFileクラスを引数にうけとる
    uiDevice.takeScreenshot(File(dir, "capture.png"))

}

UiDevice#takeScreenshotは画面全体のスクリーンショットを取得します。画面内の一部のViewのみのスクリーンショットを撮ることはできません。

PixelCopy

PixelCopyはAPI level 24で追加されたAPIで、SurfaceViewのスクリーンショットを取得できるのが特徴です。

引数にSurfaceView/Surface/WindowをとるAPIが用意されており、SurfaceとWindowを引数にとるAPIは、API level 26で追加されました。

その新たに追加されたWindowを引数にとるAPIを利用することで、画面全体や任意のViewのスクリーンショットを取得することができます。


// 画面全体のスクリーンショットを取得する例
fun capture(activity: Activity) {

    val window = activity.window
    val view = window.decorView.rootView

    // スクリーンショットを取得したいViewサイズにあわせたBitmapを作成する
    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
    PixelCopy.request(
            window,
            bitmap, // 保存先Bitmapの指定
            { copyResult ->
                if (copyResult == PixelCopy.SUCCESS) {
                    // bitmap#compressでファイルに書き出し
                }
            },
            Handler()
    )
}

また、PixelCopyはRectを引数にうけとるAPIも用意されていので、画面内の一部のViewに限定したスクリーンショットを取得することもできます。

fun captureView(view: View, activity: Activity) {

    val window = activity.window

    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)

    // Windowの中のViewの位置を取得する
    val viewLocation = IntArray(2)
    view.getLocationInWindow(viewLocation)

    PixelCopy.request(
            window,
            Rect( // Viewの位置をPixelCopyにわたすことでスクリーンショットを取得する範囲を限定する
                    viewLocation[0],
                    viewLocation[1],
                    viewLocation[0] + view.width,
                    viewLocation[1] + view.height
            ),
            bitmap,
            { copyResult ->
                if (copyResult == PixelCopy.SUCCESS) {
                    // bitmap#compressでファイルに書き出し
                }
            },
            Handler()
    )
}

スクリーンショットを比較する

ここまで、Canvasを使ったViewの書き出し・PixelCopy・UiDevice#takeScreenshotの3つのAPIの使い方を見てきました。

さっそくこれらを使って、次の画面のスクリーンショットを取得します。

Screenshot_1607916288.png

結果は以下のとおりです。

Canvas PixelCopy UiDevice
canvas1.png pixel_copy1.png uiautomator1.png

シンプルな画面ですが、取得できたスクリーンショットに差分がでています。
CanvasのスクリーンショットにはActionBarの下のEvaluationが反映されていません。
また、UiDeviceのみステータスバー内のコンテンツ(時間や電池残量など)も含まれています。

ここでは特に、Viewから書き出されたBitmapにはShadowやOutline Clipが反映されないことに注意が必要です。

ダイアログのスクリーンショットを比較する

次に、Dialogを表示している画面のスクリーンショットを取得します。

Screenshot_1607916651.png

結果は次のようになりました。

Canvas PixelCopy UiDevice
canvas2.png pixel_copy2.png uiautomator2.png

CanvasとPixelCopyではダイアログが表示されていません。これは、スクリーンショットを取得対象のActivityとダイアログのWindowが異なるためです。
一方、UiDevice#takeScreenshotはActivityに依存せずに画面全体を取得できるため、ダイアログも含まれたスクリーンショットが取得できています。

CanvasとPixelCopyでこの問題を解決するためには、リフレクションを使ってGlobalなWindowManagerからすべてのWindowを取得する必要があります。
FalconというスクリーンショットLibraryは、上記の実装をすることで、ダイアログを含んだスクリーンショットを取得できることができます。
Falconで撮影したスクリーンショットは次のようになります。(一方、Shadowは含まれていません)

falcon2.png

自前でダイアログを含むスクリーンショットを取得する仕組みを実装する場合は、Falconの実装が参考になると思います。

SurfaceViewを含む画面のスクリーンショット比較する

次に、SurfaceViewを含む画面のスクリーンショットを取得します。カメラのPreviewがSurfceViewで実装されています。

Screenshot_1607915205.png

結果は次のようになりました。

Canvas PixelCopy UiDevice
canvas3.png pixel_copy3.png uiautomator3.png

意外なことに、PixelCopyでSurfaceViewの中身がキャプチャできていません。
PixelCopyはSurfaceやSurfaceViewを引数にとる場合だとSurfaceViewの中身をキャプチャできるのですが、Windowを引数にとるAPIを使用した場合だとうまく取得できないようです。

(PixelCopyでSurfaceViewを引数にとった場合のスクリーンショット)
pixel_copy_surface.png

UiDevice#takeScreenshotはここでも安定してSurfaceViewの中身も含んだスクリーンショットが取得できています。

その他MediaProjectionを利用すると、このような画面でも正確なスクリーンショットを取得することができます。ただ、MediaProjectionは利用時にダイアログが表示されて操作が必要だったりと、テストコードでは若干使いにくいAPIです。

(MediaProjection利用時のダイアログ)

Screenshot_20201214-114351.png

個人的には、素直にUiDevice#takeScreenshotを使ったほうが簡単だと思っています。

まとめ

ここまでの結果をまとめると次のようになります。

API API Levelの制限 一部のViewのキャプチャ Shadow・Outline Clip等 ダイアログ SurfaceView
Canvas なし
(リフレクションを使用すれば可)
PixelCopy 24
(一部のAPIは26)

(リフレクションを使用すれば可)

(一部のAPIで可)
UiDevice 17から

実際のアプリと同じ見た目のスクリーンショットを取得するためには、これらのAPIを適切に選択することが必要です。
本記事が、スクリーンショット取得の手段を選定する際に助けになれば幸いです。

Mobility Technologies Advent Calendar 2020 の15日目は、reki2000さんによる 自作Cコンパイラのセルフホストに成功した話 です。お楽しみに!

22
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
13