JavaScriptを有効にしてください

【kotlin】Androidでスクリーンショットを実装する方法(サンプルプロジェクトあり)

 ·  ☕ 6 分で読めます  ·  ✍️ saiki

趣味開発でスクリーンショットを取ろうとしてググってたらなんかやけに複雑で気が滅入ってたんですが実際やってみたら存外ちょろかったので記します。

サンプルもあるのでそのまま動くよ。

では、いってみましょう。

先にサンプル置いときますね。

https://github.com/sasasaiki/android-kotlin-screenshot-sample

目次

対象読者

Androidアプリ開発しててスクリーンショット取りたいと思ってる人。

Kotlinのスクリーンショットサンプルを探してる人。

 

参考

こちらの記事を参考にさせていただきました。

とても参考になりました。本当にありがとうございます。。。!

こちらの記事の実装から、間に挟んであるserviceが私には必要なかったので削除して(一応別ブランチにserviceを挟んだものもありますが)、画像をキャッシュするところまで実装を加えて自分好みにごにょごにょした感じになっております。

あと、サービスでも結果を受け取れるようにsendBroadcasを使ってキャプチャ終了イベントを受け取ってます。(ちょっとオシャレっぽくないですか?)

スクリーンショットの流れ

Android 5.0 (Lollipop・Android SDK Level 21)からMediaProjection APIというスクリーンショットやキャプチャを撮るためのAPIが使えるようになったらしいのでそれを使います。

流れとしては、

Activityを起動->mediaProjectionManagerを使ってScreenCapture用のActivityを起動->取得できるBitmapをよしなにどうぞ

って感じです。

実装

CaptureActivity

こいつを任意のActivityやSeviceからstartすることでスクリーンショットを取ります。

mediaProjectionManagerを使ってのScreenCapture用のActivityの起動と受け取ったBitmapの保存、キャプチャ終了をsendMessageしたりします。

今回は受け取ったBitmapをキャッシュに保存しています。キャッシュのコードはもう少し後に出てきます。

package net.rennsyuu.screenshotkotlinsample

import android.app.Service
import android.content.Intent
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import net.rennsyuu.screenshotkotlinsample.common.ImageCache

class CaptureActivity : AppCompatActivity() {

    companion object {
        const val REQUEST_CAPTURE = 1
        const val END_CAPTURE_ACTION_NAME = "ON_END_CAPTURE"
    }

    private var projection: MediaProjection? = null
    private lateinit var mediaProjectionManager: MediaProjectionManager
    private val capture = Capture(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        //ScreenCaptureService(Intent)を実行した直後ここに入ってくる
        if (requestCode == REQUEST_CAPTURE) {
            if (resultCode == RESULT_OK) {
                projection = mediaProjectionManager.getMediaProjection(resultCode, data)

                //初回の許可確認ダイアログが出た場合に閉じきる前に取られてしまうので少しだけ待つ
                Thread.sleep(100)

                doCapture()
            } else {
                projection = null
                Toast.makeText(this, "キャプチャに失敗しました", Toast.LENGTH_SHORT).show()
            }
        }
        finish()
    }

    private fun doCapture() {
        projection?.let {
            capture.run(it) {bitMap ->
                //キャプチャを止めないと呼ばれ続ける
                disableCapture()
                // save bitmap
                ImageCache.put(ImageCache.Key.TmpScreenShot.str,bitmap = bitMap)
                sendMessage()
                finish()
            }
        }
    }

    private fun sendMessage() {
        val broadcast = Intent()
        broadcast.action = END_CAPTURE_ACTION_NAME
        baseContext.sendBroadcast(broadcast)
    }

    private fun disableCapture() {
        capture.stop()
        projection = null
    }

    override fun onDestroy() {
        super.onDestroy()
        disableCapture()
    }
}

Capture

先ほどのCaptureActivityで生成したMediaProjectionとやらからスクリーンの情報を受け取っり、bitmapを生成しCallBackします。

ここでスクリーンショットの範囲などを指定することができそうです。(今回は何も考えず画面全体を撮っています)

package net.rennsyuu.screenshotkotlinsample

import android.content.Context
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection

class Capture(private val context: Context) : ImageReader.OnImageAvailableListener {

    private var display: VirtualDisplay? = null
    private var onCaptureListener: ((Bitmap) -> Unit)? = null

    fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) {
        this.onCaptureListener = onCaptureListener
        if (display == null) {
            display = createDisplay(mediaProjection)
        }
    }

    private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay {
        context.resources.displayMetrics.run {
            val maxImages = 2
            val reader = ImageReader.newInstance(
                    widthPixels, heightPixels, PixelFormat.RGBA_8888, maxImages)
            reader.setOnImageAvailableListener(this@Capture, null)
            return mediaProjection.createVirtualDisplay(
                    "Capture Display", widthPixels, heightPixels, densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                    reader.surface, null, null)
        }
    }

    override fun onImageAvailable(reader: ImageReader) {
        if (display != null) {
            onCaptureListener?.invoke(captureImage(reader))
        }
    }

    private fun captureImage(reader: ImageReader): Bitmap {
        val image = reader.acquireLatestImage()
        context.resources.displayMetrics.run {
            image.planes[0].run {
                val bitmap = Bitmap.createBitmap(
                        rowStride / pixelStride, heightPixels, Bitmap.Config.ARGB_8888)
                bitmap.copyPixelsFromBuffer(buffer)
                image.close()
                return bitmap
            }
        }
    }

    fun stop() {
        display?.release()
        display = null
        onCaptureListener = null
    }
}

Cache

bitmapをそれ用に用意されているクラスLruCacheを使ってキャッシュします。

今回は最大一枚に制限しています。

bundleとか使って取り回してもいいのかもしれませんがまあキャッシュに持たせとけばどこからでもアクセスできて楽なのでとりあえずキャッシュに載せてます。

ストレージに保存する場合は別に使う必要はないです。

package net.rennsyuu.screenshotkotlinsample.common

import android.graphics.Bitmap
import android.support.v4.util.LruCache

object ImageCache {

    enum class Key(val str:String){
        TmpScreenShot("TmpScreenShot")
    }

    //キャッシュサイズをbyteで制限する場合
//    private const val CACHE_SIZE_BASE = 5
//    private const val CACHE_SIZE = CACHE_SIZE_BASE * 1024 * 1024

    private val sLruCache: LruCache<String, Bitmap>

    init {
        //キャッシュサイズをbyteで制限する場合、CACHE_SIZEを渡しsizeOfをoverrideする
        sLruCache = object : LruCache<String, Bitmap>(1) {
//            override fun sizeOf(key: String, value: Bitmap): Int {
//                return value.rowBytes * value.height
//            }
        }
    }


    operator fun get(key: String): Bitmap? {
        return sLruCache.get(key)
    }

    fun put(key: String, bitmap: Bitmap) {
        sLruCache.put(key, bitmap)
    }

    fun remove(key: String) {
        sLruCache.remove(key)
    }
}

styles.xml

CaptureActivityがスクリーンショットに写り込んでしまってもしょうがないのでアクティビティを透明にするstyleを定義します。

styles.xmlは最初からあるのでそこに以下を追記します。

<resources>
    <!-- ここら辺に最初っから色々書いてある -->

    <!-- ここから -->
    <!-- 透明背景設定 -->
    <drawable name="transparent">#00000000</drawable>

    <style name="Theme.TranslucentBackground" parent="@style/Theme.AppCompat.NoActionBar">
        <item name="android:windowBackground">@drawable/transparent</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@android:style/Animation</item>
    </style>
    <!-- ここからまで -->

</resources>

でAndroidManifest.xmlのCaptureActivityのところをこうします。

<activity
            android:name=".CaptureActivity"
            android:theme="@style/Theme.TranslucentBackground" />

これで準備はオッケー

使い方

基本的にはCaptureActivityを実行するだけですが今回はキャプチャ終了を受け取るためにReceiverをセットしたりします。

package net.rennsyuu.screenshotkotlinsample

import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.support.v7.app.AppCompatActivity

import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*
import android.content.BroadcastReceiver
import android.content.Context
import net.rennsyuu.screenshotkotlinsample.common.ImageCache
import android.content.IntentFilter


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        //ボタンを押したらスクリーンショットを撮る
        fab.setOnClickListener {
            saveScreenShot()
        }

        //サービスで処理させるデモ。なくてもよろしい
        val intent = Intent(this, MyService::class.java)
        startService(intent)

        // receiverをセットする
        val receiver = CaptureEndReceiver()
        val filter = IntentFilter()
        filter.addAction(CaptureActivity.END_CAPTURE_ACTION_NAME)
        registerReceiver(receiver, filter)
    }

    private fun saveScreenShot(){
        val intent = Intent(this, CaptureActivity::class.java)
        startActivityForResult(intent,CaptureActivity.REQUEST_CAPTURE)
    }

    fun setImage(image:Bitmap){
        image_view.setImageBitmap(image)
    }

    inner class CaptureEndReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val bitMap = ImageCache[ImageCache.Key.TmpScreenShot.str]
            bitMap?.let { setImage(it) }
        }
    }

}

 

撮ったスクリーンショットを即ImageViewにセットしてます。

一応レイアウトも置いておきますがAndroidStudioが作ったものにImageViewを追加しただけです。

activity_main

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

content_main

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:contentDescription="@string/app_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@mipmap/ic_launcher" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

 

以上です。

まとめ

ということでスクリーンショットを撮る方法でした。

まあ順番に見ていけばそんなに難しくないですね。

後サンプルが多分そのまま動くと思うのでぜひ使ってもらえればと思います。

with-serviceブランチにサービスをワンクッション入れたものも入ってます。

https://github.com/sasasaiki/android-kotlin-screenshot-sample

 

ではまた。

 

共有

saiki
著者
saiki
Android App Developper