はじめに
Androidアプリで他のアプリやホーム画面などのスクリーンショットを撮りたい事案が発生したので、やり方を記しておきます。
動作環境
Kotlin 1.6.21
AndroidStudio Bumblebee(2021.1.1)
CompileSdkVersion 32
注意点
設定アプリでは画面上にViewを出せないので、この記事の方法ではスクリーンショットを撮れません。
サンプルアプリ
記事内では説明のため省略している箇所がたくさんありますので、実際に試す場合はぜひサンプルアプリを見てみてください。
ScreenshotOutsideApp
実装
Permission設定
AndroidManifestに以下のPermissionを追加しておきます。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx.yyy.zzz">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>
アプリを閉じてもViewを表示できるようにする
アプリを閉じてもスクリーンショット用のボタンを表示しておくために、Service
を使用して常に表示されるFloatingViewをつくります。
1. View生成
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val view = ImageView(context).apply {
setImageResource(R.drawable.ic_baseline_photo_camera_24)
setBackgroundResource(R.drawable.shape_circle_filled)
val padding = (16 * resources.displayMetrics.density).toInt()
setPadding(padding, padding, padding, padding)
}
private val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
TYPE_APPLICATION_OVERLAY
を使用することで、どのActivityよりも上に表示されますが、ステータスバーやIMEなどの重要なシステムウィンドウよりは下位に表示されます。
2. Notification生成
バックグラウンドでServiceが起動していることをユーザーに知らせるため、Notification(通知)を設定します。
val id = "floating_view_channel"
val name = "Floating View"
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager = getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val activityIntent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, activityIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(
this,
0,
activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
val notification = Notification.Builder(this, id)
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(FloatingViewService::class.simpleName)
.setContentText("スクリーンショット用ボタン表示中")
.build()
3. Service起動
ActivityからServiceを起動しますが、起動前にOverlayのPermissionが許可されているかを確認します。
許可されていなかった場合、設定画面に飛ばすようにしています。
private var launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
startFloatingViewService()
}
private fun startFloatingViewService() {
if (Settings.canDrawOverlays(this)) {
val serviceIntent = Intent(this, FloatingViewService::class.java)
.setAction(FloatingViewService.ACTION_START)
startForegroundService(serviceIntent)
} else {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
)
startActivity(intent)
launcher.launch(intent)
}
}
起動できたら、ServiceのonStartCommand()
で先ほど設定したNotificationをスタートさせ、FloatingViewを表示します。
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.let {
startForeground(notificationId, notification)
windowManager.addView(view, params)
}
return Service.START_STICKY
}
スクリーンショット実行
1. MediaProjectionManager取得
Service.MEDIA_PROJECTION_SERVICE
を使ってMediaProjectionManagerを取得します。
mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
2. MediaProjectionのPermission許可のダイアログを表示
startActivityForResult()
の第一引数にmediaProjectionManager.createScreenCaptureIntent()
を渡して、Permission許可のダイアログを出します。
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE)
3. Service起動
上記のダイアログでのユーザーの選択をonActivityResult()
でキャッチし、許可されていたらServiceを起動します。
このとき、resultCode
とdata
をとっておきます。あとでMediaProjectionを取得するときに必要になります。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CAPTURE) {
mResultCode = resultCode
mData = data
if (resultCode == Activity.RESULT_OK) {
val captureServiceIntent = Intent(this, CaptureService::class.java)
.setAction(CaptureService.ACTION_START)
startService(captureServiceIntent)
}
}
}
4. Notification生成
FloatingViewと同じようにNotificationを設定します。
val id = "capture_service_channel"
val name = "Capture Service"
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager = getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val activityIntent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, activityIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(
this,
0,
activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
val notification = Notification.Builder(this, id)
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(CaptureService::class.simpleName)
.setContentText("スクリーンショットサービス起動中")
.build()
startForeground(notificationId, notification)
5. MediaProjection取得
先ほどとっておいたresultCode
とdata
を使って、MediaProjectionを取得します。
mediaProjection = mediaProjectionManager.getMediaProjection(mResultCode, mData)
6. FloatingViewタップ時にMediaProjectionを使ってスクリーンショット実行
mediaProjection?.let { mediaProjection ->
capture.run(mediaProjection) { bitmap ->
// サンプルアプリでは端末に保存するところまで実装してあります
saveBitmap(bitmap)
capture.stop()
}
}
class Capture(private val context: Context) : ImageReader.OnImageAvailableListener {
private var display: VirtualDisplay? = null
private var onCaptureListener: ((Bitmap) -> Unit)? = null
override fun onImageAvailable(reader: ImageReader) {
if (display != null) {
onCaptureListener?.invoke(captureImage(reader))
}
}
fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) {
this.onCaptureListener = onCaptureListener
if (display == null) {
display = createDisplay(mediaProjection)
}
}
fun stop() {
display?.release()
display = null
onCaptureListener = null
}
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
)
}
}
private fun captureImage(reader: ImageReader): Bitmap {
val image = reader.acquireLatestImage()
context.resources.displayMetrics.let { displayMatrics ->
image.planes[0].run {
// PixelFormat.RGBA_8888がエラーになる場合があるが、ビルドできる
val bitmap = Bitmap.createBitmap(
rowStride / pixelStride, displayMatrics.heightPixels, Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
image.close()
return bitmap
}
}
}
}
おわりに
参考になるページはありましたが過去のものが多く苦労しました。。
誰かの助けになれば幸いです!
参考ページ
[Android] WindowManagerを使ってServiceから画像を表示させ続ける
常に画面の最前面に表示されたままになる View を作る (TYPE_APPLICATION_OVERLAY)
フローティングアプリを作るためのはじめの一歩
Foreground Serviceの基本
ANDROID 5.0 アプリからスクリーンショットを撮影する
画面のスナップショットを撮影する
【kotlin】Androidでスクリーンショットを実装する方法(サンプルプロジェクトあり)