今日はAndroid開発におけるファジング技術について、より深く見ていきます。このテーマに関するいくつかの記事を読み、StackOverFlowで関連するチュートリアルの投稿をいくつか見ました。
なぜこのファジー技術を学ぶのですか?
RomanNurikが開発したYahooのとても素敵です。私は彼らのデザインがとても好きです。
マーク・アリソンの投稿投稿アドレス)に触発されて、この記事を書きました。
これは以下に示す効果を完成させるために必要です:
[]
予備知識
まず、必要なファイルについて説明してください。複数のフラグメントを含むViewPagerを持つメインのアクティビティが必要です。
これはメインアクティビティのレイアウトファイルの内容です:
<android.support.v4.view.ViewPager
xmlns:android="http://..com/apk/res/android"
xmlns:tools="http://..com/tools"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.paveldudka.MainActivity" />
これがFragmentのレイアウトファイルの内容です:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://..com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/picture"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/text"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="My super text"
android:textColor="<a href="http://..com/members/android/" rel="nofollow">@android</a>:color/white"
android:layout_gravity="center_vertical"
android:textStyle="bold"
android:textSize="48sp" />
<LinearLayout
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#7f000000"
android:orientation="vertical"
android:layout_gravity="bottom"/>
</FrameLayout>
ImageViewとTextViewが真ん中にあり、エフェクト表示とテストを行うコントロールがいくつかあるだけのレイアウトです。
最も一般的なファジー技法はこうです:
- TextViewのバックレイヤーから背景の一部を取り込みます;
- ぼかし;
- ぼかした部分をTextViewの背景として設定します。
レンダースクリプト
Androidでぼかしを実装するには?一番の答えはRenderscriptです。Renderscriptは強力なグラフィック「エンジン」で、その基本原理については触れませんし、この記事の範囲外です。
まず下のコードを見てください:
public class RSBlurFragment extends Fragment {
private ImageView image;
private TextView text;
private TextView statusText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
image = (ImageView) view.findViewById(R.id.picture);
text = (TextView) view.findViewById(R.id.text);
statusText = addStatusText((ViewGroup) view.findViewById(R.id.controls));
applyBlur();
return view;
}
private void applyBlur() {
image.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
image.getViewTreeObserver().removeOnPreDrawListener(this);
image.buildDrawingCache();
Bitmap bmp = image.getDrawingCache();
blur(bmp, text);
return true;
}
});
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float radius = 20;
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()),
(int) (view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft(), -view.getTop());
canvas.drawBitmap(bkg, 0, 0, null);
RenderScript rs = RenderScript.create(getActivity());
Allocation overlayAlloc = Allocation.createFromBitmap(
rs, overlay);
ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(
rs, overlayAlloc.getElement());
blur.setInput(overlayAlloc);
blur.setRadius(radius);
blur.forEach(overlayAlloc);
overlayAlloc.copyTo(overlay);
view.setBackground(new BitmapDrawable(
getResources(), overlay));
rs.destroy();
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
@Override
public String toString() {
return "RenderScript";
}
private TextView addStatusText(ViewGroup container) {
TextView result = new TextView(getActivity());
result.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
result.setTextColor(0xFFFFFFFF);
container.addView(result);
return result;
}
}
- Fragmentが作成されたら、レイアウトファイルを読み込み、レイアウトファイルで定義したLinearLayoutにTextViewを追加し、最後にイメージをぼかしました。
- applyBlur()関数の中で、onPreDrawListener()を登録しました。applyBlur()メソッドが呼ばれたときにはまだインターフェイスがレイアウトされていないので、このリスナーを実装する必要があります。レイアウトファイルがすべて測定され、レイアウトされ、表示されるまで待ってから操作する必要があります。
- onPreDraw()コールバック関数では、まず戻り値をfalseからtrueに変更しています。これはとても重要なことで、falseを返すと、今現れたフレームはスキップされてしまいますが、この最初のフレームを表示する必要があるので、trueを返す必要があります。
- その後、preDrawイベントをリッスンする必要がなくなったので、コールバック・メソッドを削除しました。
- 最後に、次に詳しく説明するぼかし処理を行います。
- レイアウトファイルが変更されたときに、自動的に再度ぼかしが入ることはありません。
onGlobalLayoutListener
この問題は、リスナーを登録し、レイアウトファイルが変更されたときだけぼかし直すことで解決できます。 - このファジング操作はメインスレッドで行います。実際の開発ではこんなことはできないでしょうが、とりあえず便宜上やっておきます。
さて、blur()メソッドに戻りましょう:
- まず、空のビットマップを作成し、背景の一部をそこにコピーします。その後、このビットマップをぼかし、TextViewの背景として設定します。
- このビットマップを通してキャンバスの状態を保存します;
- キャンバスを親レイアウトファイルのTextViewの位置に移動します;
- ImageViewの内容をビットマップに描画します;
- この時点で、TextViewと同じ大きさのビットマップがあり、ImageViewの一部を含んでいます;
- Renderscript のインスタンスを作成します;
- レンダースクリプトが必要とするデータスライスにビットマップのコピーを作成します;
- Renderscript blurring のインスタンスを作成します;
- 入力、半径の範囲を設定し、ぼかします;
- 処理結果をビットマップにコピーします;
- OK、ビットマップがぼかされ、TextViewの背景として設定できるようになりました;
これが処理後の効果です:
ご覧の通り、結果は悪くありませんが、57msかかりました。Androidでは1フレームのレンダリングに16ms以上かからないはずですが、UIスレッドでぼかし処理を行うと、フレームレートが17fpsに低下します。これは明らかに容認できないので、処理をAsyncTaskに移すか、別のメカニズムを使って実装する必要があります。
また、ScriptIntrinsicBlurはAPI17以上しかサポートしていませんが、Renderscriptのサポートライブラリを使用することで、APIバージョンの要件を下げることができます。
しかし、Renderscript をサポートしていない古い API バージョンをサポートする必要があります。
ファストブラー
このぼかし処理も単なるピクセル処理であることはご存じでしょうから、手作業でぼかし処理をしてみてもよいでしょう。幸い、Javaでぼかし方式を実装した例はすでにたくさんあります。あとは、比較的高速な実装を見つけるだけです。
StackOverFlowの投稿のおかげで、ぼかしを高速化するソリューションを見つけました。まずはどんなものか見てみましょう。
多くのコードは同じなので、ここではファジー処理に関する機能についてのみ説明します:
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float radius = 20;
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()),
(int) (view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft(), -view.getTop());
canvas.drawBitmap(bkg, 0, 0, null);
overlay = FastBlur.doBlur(overlay, (int)radius, true);
view.setBackground(new BitmapDrawable(getResources(), overlay));
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
実現の効果は以下の通り:
ご覧のとおり、ぼかしもかなりうまくいっています。FastBlurを使う利点は、Renderscriptへの依存を取り除けることです。しかし、ぼかし処理に147ミリ秒かかりました!これはSWのぼかしアルゴリズムで最も遅いものではありません、ガウスぼかしを使う勇気はありませんが...。
さらに詳しく
さて、どうするか考えましょう。すべてのファジー処理には精度の低下があります。 精度の低下とは何かわかりますか?そう、サイズを小さくすることです。
その場合、まずビットマップのサイズを小さくしてからぼかし、それからサイズを大きくするのはどうでしょう?というアイデアを実行してみたところ、こんな感じになりました:
Renderscriptは13ms、FastBlurは2msです!
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float scaleFactor = 1;
float radius = 20;
if (downScale.isChecked()) {
scaleFactor = 8;
radius = 2;
}
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()/scaleFactor),
(int) (view.getMeasuredHeight()/scaleFactor), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft()/scaleFactor, -view.getTop()/scaleFactor);
canvas.scale(1 / scaleFactor, 1 / scaleFactor);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(bkg, 0, 0, paint);
overlay = FastBlur.doBlur(overlay, (int)radius, true);
view.setBackground(new BitmapDrawable(getResources(), overlay));
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
このコードを見てください:
- 次にビットマップを作成する必要がありますが、これは最終的に必要となるものより8倍小さいものです。
- PaintにFILTER_BITMAP_FLAGフラグを与えたのは、ビットマップのスケーリングを扱うときにダブルバッファリングの効果が得られ、ぼかし処理がよりスムーズになるようにするためです。
- 次は同じぼかし処理ですが、今度はイメージがずっと小さく、大きさもかなり小さくなっているので、ぼかし処理は非常に高速です。
- ぼかしたイメージを背景にすると、自動的にズームインします。
FastBlur のぼかし処理が Renderscript より速いといえば、これは FastBlur がビットマップのコピー処理と同時に他の処理を実行することで時間を節約しているためです。このような処理の後、比較的高速なぼかしソリューションと原理が得られ、RenderScript への依存が取り除かれるはずです。