※この記事はpotatotips45の発表をまとめたものです。

はじめに

アプリを開発していて、ポップアップのようなUIをActivityで実装することがあると思います。その際に、背景をぼかして欲しいという依頼を受けたことはないでしょうか。iOSではUIVisualEffectViewを使うだけですが、Androidでは一工夫必要です。本記事では、Activityの背景をぼかす際の実装の方針や、苦労した点をご紹介したいと思います。

実装の方針

Activityで背景のぼかしを実現するには大まかに3ステップ必要です。

  1. 背景のキャプチャを撮る
  2. キャプチャ画像にぼかし処理を掛ける
  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という警告がでます。

サンプル

簡単な実装例です。

12月-01-2017 13-58-13.gif

GitHubでコードを公開しています。
https://github.com/niba1122/blurred-activity

最後に

Androidでもポップアップの背景をぼかすことは可能です。ただ、実装の際にはメモリやスレッド、ライフサイクルを考慮しなければいけないので、実装には工夫が必要です。