序
最近はWebページでもブラー(ぼかし)エフェクトを使用したサイトがみられるようになりましたね(今からだと旬は過ぎた感ありますが…)。
iOSだと6年前のiOS 8から UIVisualEffectView
という便利なビューが標準で用意されたので、ミニマムだとレイアウトだけで実現できてしまいます。
Layout風に書くとこんな感じ…
<UIView> <!-- UIViewはViewGroupのように子ビューを複数持てる -->
<UIScrollView>
<UIImageView>
</UIScrollView>
<UIVisualEffectView /> <!-- Androidと同じく下に書くと階層的には上に来て下にあるビューにブラーがかかる -->
</UIView>
最近Webでも backdrop-filter
によってIEを除いた最新のブラウザで使えるようになりました:
雑ですが iOS のサンプルを大まかに再現するとこうなります:
ちょっと長いので省略
<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>
本題
画像やビューのキャプチャに対してブラーをかける方法についてはすでに 記事
や 質問 がありましたが、スクロールなどリアルタイムにかける方法についてはまだ情報がまとまっていなさそうだったので、実現するライブラリについていろいろ書いていきたいと思います。
ちなみに
- 今回のような例であれば上記ライブラリであらかじめビットマップを作っておいてスクロールに合わせて動かせば擬似的に見せることは可能でした(発想の転換)
- 静止画に対するブラーの掛け方は RenderScript から CPU で Bitmap を直接触る方法などいろいろありますが、興味深いことに StackOverflow でパフォーマンスを測定した方がおられたので共有しておきます
RealtimeBlurView
ずばりな名前のライブラリでレイアウトに乗っけるだけで背景のビューにブラーがかかってくれるすごいライブラリです。説明に書いてある通りにするだけで確かに実現ができました:
サンプルコードはこちらです: https://github.com/cubenoy22/android-reatime-blur
どうやって実現しているのか
ただライブラリを紹介するだけではつまらないのでちょっと中身をみていきたいと思います。
RealtimeBlurView
XMLで配置するビューの実装から見ていきます。対象ファイルは こちら。
ブラー用のビットマップ作成
prepare() メソッドでメンバーの mBitmapToBlur をビューの大きさ分で作成していて、描画用の mBlurringCanvas
も同時に作成しています。このメソッドは後ほど紹介する onPreDraw 内で呼ばれる ようになっていて、画面の更新がされる直前かつまだ準備できていなければ実行されるようになっています。ビューのサイズ変更もここで対応できるようになっていました。
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 に対して
- ViewTreeObserver.OnPreDrawListener をセットし画面の更新を受け取れるように L301
-
background があれば上で用意した
mBitmapToBlur
へ描画 L263 - 一番肝心な draw() をコールし
mBitmapToBlur
へ描画 L265
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
と同時に同じサイズで用意 されていました。上のコードにも書いておきましたが blur() 関数ですね。
protected void blur(Bitmap bitmapToBlur, Bitmap blurredBitmap) {
mBlurImpl.blur(bitmapToBlur, blurredBitmap);
}
mBlurImpl ( BlurImpl ) は getBlurImpl メソッドで状況に応じた実装が使用されるようになっており、違いは RenderScript のパッケージが各種用意されているだけのようでした:
ScriptIntrinsicBlur を使って mBitmapToBlur
の内容にブラーを mBlurrerdBitmap
に出力させていることがわかります。
@Override
public void blur(Bitmap input, Bitmap output) {
mBlurInput.copyFrom(input);
mBlurScript.setInput(mBlurInput);
mBlurScript.forEach(mBlurOutput);
mBlurOutput.copyTo(output);
}
これで表示するための準備ができあがりました。
onDraw で mBlurredBitmap を描画する
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBlurredBitmap(canvas, mBlurredBitmap, mOverlayColor);
}
ブラーをかけた Bitmap を表示すれば完了となるわけですね!
BlurKit
こちらの方がスター数が多いですが
- onStart / onStop にコードを書かないと動かない
- 毎フレーム必ずinvalidate()をコールしていて負荷が高そう BlurLayout.java#L111
- 毎フレームごとに Bitmap を作成していて効率が悪そう BlurLayout.java#L164, BlurLayout.java#L249
という理由から個人的に RealtimeBlurView をオススメしたいです。ちなみにブラーをかけるところは ほぼ同じ でビットマップをひとつだけしか使用しないか二つ使用するかが唯一異なる点でした(RealtimeBlurViewのビットマップが二つ必要な理由についてはまだ調査していません)。
(ちょこっと追記) ほぼ同じって書きましたが RenderScript のコードも改めてみてみると毎回初期化していてここも効率化できそう。。
BlurView
こちらはレイアウト以外にコードを書く必要があるみたいです。コードは読みやすい気がします。
- ブラーをかける制御 BlockingBlurController.java
- ブラーをかける処理 RenderScriptBlur.java
全体的に RealtimeBlurView と同じような実装で設計と RenderScript のビットマップを2つ使用するかどうかの違いしかなさそうでした。気軽に使用できる点からやはり RealtimeBlurView をおすすめしたいです。RealtimeBlurViewのビットマップが二重になっているところが改善されたら今のところ最強でしょうかね(プルリク出してみようかしら…)。
終わりに
Androidのライブラリ3つを紹介しましたがいずれもビットマップを用意したりしています。おそらくiOSやWebではGPUのレンダリングパイプラインあたりだけで実現している気がしていて、Androidでもそういった高効率のアプローチができないか調査する余地がありそう、という感想を持ちました。できればGoogle純正のイケてるライブラリが出てくれると嬉しかったり。