※この記事はpotatotips45の発表をまとめたものです。
はじめに
アプリを開発していて、ポップアップのようなUIをActivityで実装することがあると思います。その際に、背景をぼかして欲しいという依頼を受けたことはないでしょうか。iOSではUIVisualEffectViewを使うだけですが、Androidでは一工夫必要です。本記事では、Activityの背景をぼかす際の実装の方針や、苦労した点をご紹介したいと思います。
実装の方針
Activityで背景のぼかしを実現するには大まかに3ステップ必要です。
- 背景のキャプチャを撮る
- キャプチャ画像にぼかし処理を掛ける
- ぼかし処理をかけたキャプチャ画像をActivityの背景に設定する
1、2に関してはBackground Blur in Androidを参考にしました。それぞれ「Getting Bitmap Ready」「Blur it!」の章で実装の仕方が紹介されています。これでできたも同然と思われるかもしれませんが、実装してみて苦労した部分があったので、次章以降で紹介していきます。
苦労した点①:ポップアップのActivityで背景のキャプチャをとれない
ポップアップのActivityでキャプチャを撮っても、背景のActivityはキャプチャされませんでした。試行錯誤の末、背景のActivityでキャプチャを撮ってからそれをポップアップのActivityに渡すようにしました。ただ、Bitmapはそのままでは容量が大きいためIntentで渡せません。今回は画像を一度ローカルストレージに保存し、そのUriをIntentで渡すようにしました。
// Activityのキャプチャを撮る
val view: View = window.findViewById(android.R.id.content)
try {
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val c = Canvas(bitmap)
view.draw(c)
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
// Bitmapをローカルストレージに保存してUriを取得
val cacheDir = weakActivity.get()?.cacheDir ?: return null
val file = File(cacheDir, CACHE_FILE_NAME)
try {
val outputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()
Uri.fromFile(file)
} catch (e: IOException) {
e.printStackTrace()
}
これで、めでたく背景のキャプチャを得ることができました。今回はやっていませんが、キャプチャを受け渡す際、Bitmap自体の容量を圧縮するのも良いかもしれません。
苦労した点②:画像保存・ぼかし処理が重い
画像保存・ぼかし処理は結構重く、機種にもよってはそれぞれ0.5秒ほどかかる場合もあります。メインスレッドをこれらを実行すると他の処理を圧迫してしまうので、全てAsyncTaskで処理することにしました。画像の処理ではContextを使うので、AsyncTaskのイニシャライザでActivityを渡すようにしました。以下のコードはぼかし処理をAsynkTaskで行うようにした例です。
// 非同期でUriから画像を取得してぼかし処理を掛ける
private class AsyncBlurredCaptureLoader(activity: BlurredModalActivity) : AsyncTask<Uri, Unit, Drawable>() {
private val activity = WeakReference(activity)
override fun doInBackground(vararg params: Uri?): Drawable? {
val uri = params[0] ?: return null
val capture = loadCapture(uri) ?: return null
val renderScript = activity.get()?.let { RenderScript.create(it) } ?: return null
val blurredCapture = RSBlurProcessor(renderScript).blur(capture, 25f, 3) ?: return null
val resources = activity.get()?.resources ?: return null
return BitmapDrawable(resources, blurredCapture)
}
override fun onPostExecute(result: Drawable?) {
super.onPostExecute(result)
activity.get()?.onLoadBlurredCapture(result)
}
private fun loadCapture(uri: Uri): Bitmap? {
try {
val inputStream = activity.get()?.contentResolver?.openInputStream(uri) ?: return null
return BitmapFactory.decodeStream(inputStream)
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
}
ここで一つポイントなのはActivityをWeakReferenceで持っている点です。ここでActivityを通常のプロパティで持ってしまうと、Activityが破棄された時に適切にGCされなくなる恐れがあります。実際、Android StudioでもThis field leaks a context object
という警告がでます。
サンプル
簡単な実装例です。
GitHubでコードを公開しています。
https://github.com/niba1122/blurred-activity
最後に
Androidでもポップアップの背景をぼかすことは可能です。ただ、実装の際にはメモリやスレッド、ライフサイクルを考慮しなければいけないので、実装には工夫が必要です。