背景
画面内に表示されている特定の情報を画像として保存し、後から見返せるようにしてユーザー利便性を向上させたいという要件がありました。(例えば、ゲームアプリのユーザー情報だったり、乗換案内アプリの検索結果だったり。)
この記事ではこの要件の実現方法について、下記のポイントを解説いたします。
- スクリーンショットの撮影方法
- ギャラリーへの反映方法
コード解説
全てのソースコードはGithubで公開しています。
クローンして手元で動かしてみると処理の流れがイメージしやすいと思います。
https://github.com/umechanhika/playground/tree/master/android/AddScreenshotToGallery
今回のポイントとなるコードは、スクリーンショットの撮影から保存までを行なっているScreenshotUtil.ktです。
package com.example.addscreenshottogallery.ui.util
import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.View
import androidx.core.view.drawToBitmap
import java.io.File
import java.io.FileOutputStream
object ScreenshotUtil {
private const val DIRECTORY_NAME = "AddScreenshotToGallery"
private const val FILE_EXTENSION = ".jpg"
fun take(
contentResolver: ContentResolver,
targetView: View,
fileName: String,
completion: (Bitmap, Uri) -> Unit
) {
// 対象のViewをBitmapに書き出します、スクリーンショットの撮影処理はこれだけです
val bitmap = targetView.drawToBitmap()
targetView.draw(Canvas(bitmap))
// ギャラリーへの反映時に必要な情報を格納している値です
// AndroidQ以降とそれより前で設定可能なKeyが異なるため、
// ここでは共通部分のみ生成してif文内でそれぞれに必要な値を追加しています
val contentValues = createContentValues(fileName)
val uri: Uri
if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) {
contentValues.apply {
// 画像の保存先を指定します(AndroidQより前と指定方法が異なります)
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/$DIRECTORY_NAME"
)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
contentResolver.run {
uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
?: return
// AndroidQ以降では画像の書き出し前にギャラリーへの登録(正確に言うとMediaStoreへの登録)を
// 済ませてから該当Uriに画像を書き出します(処理の順番がAndroidQより前と異なります)
openOutputStream(uri).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
update(uri, contentValues, null, null)
}
} else {
// AndroidQより前の処理では一部メソッドがDeprecatedになっていますが、
// Q以降の処理に互換性がないためSDK_INTによって処理を分岐させています
// スクリーンショット画像書き出し用のディレクトリ・ファイルを準備
val directory = File(
// ファイルの書き出し先はいくつか候補がありますが、
// アプリを削除してもファイルが消えない、外部アプリからアクセス可能という理由から、
// getExternalStoragePublicDirectoryを選択しています
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
DIRECTORY_NAME
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "$fileName$FILE_EXTENSION")
// Bitmapをファイルに書き出します
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }
// 書き出した画像をギャラリーに反映させています
contentResolver.run {
contentValues.put(MediaStore.Images.Media.DATA, file.absolutePath)
uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return
}
}
completion(bitmap, uri)
}
/**
* [Build.VERSION_CODES.Q]以上とそれより前で設定できる値が違うため、ここでは共通部分のみ設定
*/
private fun createContentValues(name: String) = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$name$FILE_EXTENSION")
put(MediaStore.Images.Media.TITLE, name)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1_000)
}
}
GIF
参考リンク
AndroidQからのストレージアクセスに関する変更点や、MediaStoreの保存処理の変更点についての情報を記載してくださっています
https://medium.com/@star_zero/android-q%E3%81%AEscoped-storage%E3%81%AB%E3%82%88%E3%82%8B%E5%A4%89%E6%9B%B4-afe41cde9f35
https://medium.com/@mag0716/scoped-storage-%E8%AA%BF%E6%9F%BB%E3%83%A1%E3%83%A2-8a13c70d3ec8
Androidのストレージに関する公式ドキュメントです(日本語ページでは一部情報が足りていない場合があるため、言語設定は英語で閲覧することをおすすめします)
https://developer.android.com/training/data-storage
AndroidQ以降とそれより前のバージョンでの実装の違いについて記載されています
https://stackoverflow.com/questions/56904485/how-to-save-an-image-in-android-q-using-mediastore