趣味開発でスクリーンショットを取ろうとしてググってたらなんかやけに複雑で気が滅入ってたんですが実際やってみたら存外ちょろかったので記します。
サンプルもあるのでそのまま動くよ。
では、いってみましょう。
先にサンプル置いときますね。
https://github.com/sasasaiki/android-kotlin-screenshot-sample
目次
- 対象読者
- 参考
- スクリーンショットの流れ
- 実装
<li> <a href="#i-5">使い方</a> </li> </ul> </li> <li> <a href="#i-6">まとめ</a> </li>
対象読者#
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
ではまた。