Androidでもリアルタイムにブラーをかける

# 序

最近はWebページでもブラー（ぼかし）エフェクトを使用したサイトがみられるようになりましたね（今からだと旬は過ぎた感ありますが…）。
iOSだと6年前のiOS 8から `UIVisualEffectView` という便利なビューが標準で用意されたので、ミニマムだとレイアウトだけで実現できてしまいます。

![ios.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282588/0e49f3aa-dbb8-b431-aee9-69ff0774341b.gif)

Layout風に書くとこんな感じ…

```xml
<UIView> <!-- UIViewはViewGroupのように子ビューを複数持てる -->
  <UIScrollView>
    <UIImageView>
  </UIScrollView>
  <UIVisualEffectView /> <!-- Androidと同じく下に書くと階層的には上に来て下にあるビューにブラーがかかる -->
</UIView>
```

最近Webでも `backdrop-filter` によってIEを除いた最新のブラウザで使えるようになりました:

![web.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282588/1f01da58-2b15-ccbf-f876-349ed280c619.gif)

雑ですが iOS のサンプルを大まかに再現するとこうなります:

<details><summary>ちょっと長いので省略</summary><div>

```HTML
<html>
  <head>
    <style type="text/css">
      img {
        object-fit: cover;
        width: 100%;
        height: 200vh;
      }

      .overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        display: flex;
      }

      .box {
        margin: auto;
        width: 75vw;
        height: 75vw;
        backdrop-filter: blur(24px);
        -webkit-backdrop-filter: blur(24px); /* Safari */
       background-color: #ffffff5f;
     }
    </style>
  </head>

  <body>
    <img src="...">
    <div class="overlay">
      <div class="box"></div>
    </div>
  </body>
</html>
```

</div></details>

# 本題

画像やビューのキャプチャに対してブラーをかける方法についてはすでに [記事](https://qiita.com/magie-pooh/items/cdaf2b6af535e6ff56bb) 
 や [質問](https://stackoverflow.com/questions/31808955/android-blur-view-blur-background-behind-the-view) がありましたが、スクロールなどリアルタイムにかける方法についてはまだ情報がまとまっていなさそうだったので、実現するライブラリについていろいろ書いていきたいと思います。

### ちなみに

* 今回のような例であれば上記ライブラリであらかじめビットマップを作っておいてスクロールに合わせて動かせば擬似的に見せることは可能でした（発想の転換）
* 静止画に対するブラーの掛け方は RenderScript から CPU で Bitmap を直接触る方法などいろいろありますが、興味深いことに [StackOverflow](https://stackoverflow.com/a/23119957) でパフォーマンスを測定した方がおられたので共有しておきます

## RealtimeBlurView

* https://github.com/mmin18/RealtimeBlurView
* :star: 2501

ずばりな名前のライブラリでレイアウトに乗っけるだけで背景のビューにブラーがかかってくれるすごいライブラリです。説明に書いてある通りにするだけで確かに実現ができました:

![android.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282588/f4c43e80-7a90-f12b-bee1-6f406dbe68af.gif)

サンプルコードはこちらです: https://github.com/cubenoy22/android-reatime-blur

### どうやって実現しているのか

ただライブラリを紹介するだけではつまらないのでちょっと中身をみていきたいと思います。

#### RealtimeBlurView

XMLで配置するビューの実装から見ていきます。対象ファイルは [こちら](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java)。

#### ブラー用のビットマップ作成

[prepare()](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L169) メソッドでメンバーの [mBitmapToBlur](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L36) をビューの大きさ分で作成していて、描画用の `mBlurringCanvas` も同時に作成しています。このメソッドは後ほど紹介する [onPreDraw 内で呼ばれる](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L242) ようになっていて、画面の更新がされる直前かつまだ準備できていなければ実行されるようになっています。ビューのサイズ変更もここで対応できるようになっていました。

```java
protected boolean prepare() {
    // 省略
    int scaledWidth = Math.max(1, (int) (width / downsampleFactor));
    int scaledHeight = Math.max(1, (int) (height / downsampleFactor));

    if (mBlurringCanvas == null || mBlurredBitmap == null
        || mBlurredBitmap.getWidth() != scaledWidth
        || mBlurredBitmap.getHeight() != scaledHeight) {
        mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
    }
    mBlurringCanvas = new Canvas(mBitmapToBlur);
    mBlurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
}
```


#### ブラーが必要なエリアをビットマップに転写

Activity の [Window の decorView](https://developer.android.com/reference/android/view/Window#getDecorView()) に対して

* [ViewTreeObserver.OnPreDrawListener](https://developer.android.com/reference/android/view/ViewTreeObserver.OnPreDrawListener) をセットし画面の更新を受け取れるように :point_right: [L301](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L301)
* [background](https://developer.android.com/reference/android/view/View#getBackground()) があれば上で用意した `mBitmapToBlur` へ描画 :point_right: [L263](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L263)
* 一番肝心な [draw()](https://developer.android.com/reference/android/view/View#draw(android.graphics.Canvas)) をコールし `mBitmapToBlur` へ描画 :point_right: [L265](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L265)

```java
private final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        // 省略
        if (decor != null && isShown() && prepare()) {
            if (decor.getBackground() != null) {
                decor.getBackground().draw(mBlurringCanvas);
            }
            decor.draw(mBlurringCanvas);
            blur(mBitmapToBlur, mBlurredBitmap);
        }
    }
}
```

これであらかじめ作っておいた Bitmap (mBitmapToBlur) に対してビューの内容が転写されました。面白い。

#### 転写した Bitmap に対してブラーをかける

直前の preDrawListener での一連の流れでブラーもかけています。 `mBitmapToBlur` の他にもう一つ `mBlurredBitmap ` という Bitmap が用意されています。このビットマップは実は `mBitmapToBlur` と[同時に同じサイズで用意](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L204) されていました。上のコードにも書いておきましたが blur() 関数ですね。

```java
protected void blur(Bitmap bitmapToBlur, Bitmap blurredBitmap) {
    mBlurImpl.blur(bitmapToBlur, blurredBitmap);
}
```

mBlurImpl ( [BlurImpl](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/BlurImpl.java) ) は [getBlurImpl](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/RealtimeBlurView.java#L64) メソッドで状況に応じた実装が使用されるようになっており、違いは RenderScript のパッケージが各種用意されているだけのようでした:

* [AndroidStockBlurImpl](https://github.com/mmin18/RealtimeBlurView/blob/master/library/src/com/github/mmin18/widget/AndroidStockBlurImpl.java)
* [AndroidXBlurImpl](https://github.com/mmin18/RealtimeBlurView/blob/aef9860e5778ae40ffa060cd29d111941ef32356/library/src-blurimpl/AndroidXBlurImpl.java)
* [SupportLibraryBlurImpl](https://github.com/mmin18/RealtimeBlurView/blob/aef9860e5778ae40ffa060cd29d111941ef32356/library/src-blurimpl/SupportLibraryBlurImpl.java)

[ScriptIntrinsicBlur](https://developer.android.com/reference/android/renderscript/ScriptIntrinsicBlur) を使って `mBitmapToBlur` の内容にブラーを `mBlurrerdBitmap` に出力させていることがわかります。

```java
@Override
public void blur(Bitmap input, Bitmap output) {
    mBlurInput.copyFrom(input);
    mBlurScript.setInput(mBlurInput);
    mBlurScript.forEach(mBlurOutput);
    mBlurOutput.copyTo(output);
}
```

これで表示するための準備ができあがりました。 

#### onDraw で mBlurredBitmap を描画する

```java
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawBlurredBitmap(canvas, mBlurredBitmap, mOverlayColor);
}
```

ブラーをかけた Bitmap を表示すれば完了となるわけですね！

## BlurKit

* https://github.com/CameraKit/blurkit-android
* :star: 3309

こちらの方がスター数が多いですが

* onStart / onStop にコードを書かないと動かない
* 毎フレーム必ずinvalidate()をコールしていて負荷が高そう :point_right: [BlurLayout.java#L111](https://github.com/CameraKit/blurkit-android/blob/master/blurkit/src/main/java/io/alterac/blurkit/BlurLayout.java#L111)
* 毎フレームごとに Bitmap を作成していて効率が悪そう :point_right: [BlurLayout.java#L164](https://github.com/CameraKit/blurkit-android/blob/master/blurkit/src/main/java/io/alterac/blurkit/BlurLayout.java#L164), [BlurLayout.java#L249](https://github.com/CameraKit/blurkit-android/blob/master/blurkit/src/main/java/io/alterac/blurkit/BlurLayout.java#L249)

という理由から個人的に RealtimeBlurView をオススメしたいです。ちなみにブラーをかけるところは [ほぼ同じ](https://github.com/CameraKit/blurkit-android/blob/master/blurkit/src/main/java/io/alterac/blurkit/BlurKit.java#L30) でビットマップをひとつだけしか使用しないか二つ使用するかが唯一異なる点でした（RealtimeBlurViewのビットマップが二つ必要な理由についてはまだ調査していません）。

(ちょこっと追記) ほぼ同じって書きましたが RenderScript のコードも改めてみてみると毎回初期化していてここも効率化できそう。。

## BlurView

* https://github.com/Dimezis/BlurView
* :star: 2479

こちらはレイアウト以外にコードを書く必要があるみたいです。コードは読みやすい気がします。

* ブラーをかける制御 :point_right: [BlockingBlurController.java](https://github.com/Dimezis/BlurView/blob/master/library/src/main/java/eightbitlab/com/blurview/BlockingBlurController.java)
* ブラーをかける処理 :point_right: [RenderScriptBlur.java](https://github.com/Dimezis/BlurView/blob/master/library/src/main/java/eightbitlab/com/blurview/RenderScriptBlur.java)

全体的に RealtimeBlurView と同じような実装で設計と RenderScript のビットマップを2つ使用するかどうかの違いしかなさそうでした。気軽に使用できる点からやはり RealtimeBlurView をおすすめしたいです。RealtimeBlurViewのビットマップが二重になっているところが改善されたら今のところ最強でしょうかね（プルリク出してみようかしら…）。

# 終わりに

Androidのライブラリ3つを紹介しましたがいずれもビットマップを用意したりしています。おそらくiOSやWebではGPUのレンダリングパイプラインあたりだけで実現している気がしていて、Androidでもそういった高効率のアプローチができないか調査する余地がありそう、という感想を持ちました。できればGoogle純正のイケてるライブラリが出てくれると嬉しかったり。