2012年8月23日木曜日

Android 大きい画像を効果的に読み込む

Loading Large Bitmaps Efficiently
の内容なのですが、補足も入れてメモっておきます。

端的にいうと、

実際に表示するサイズより大きい Bitmap を読みこむのはメモリの無駄
(拡大させるとかなら話は別だけど)

・高解像度のカメラで取られた写真は往々にしてディスプレイのピクセルサイズより大きい
・サムネイルとして使うのに元のサイズのまま読み込むのはばかげてる
・大きいサイズの Bitmap をメモリに展開したら OutOfMemoryException になる


ステップとしては3つ

1. メモリに Bitmap 展開せずに、サイズや MimeType だけを取得する
2. 1. の情報をもとにサブサンプルにサイズを決める
3. 2. で決めたサブサンプルで Bitmap をメモリに読み込む


1. メモリに Bitmap 展開せずに、サイズや MimeType だけを取得する

BitmapFactory のデコードメソッド(decodeFile(), decodeResources(), decodeByteArray() など)は引数として BitmapFactory.Options を取るようになっているものがあります。

この BitmapFactory.Options の inJustDecodeBounds プロパティに true をセットしておくと、Bitmap がメモリに展開されません。ただし、BitmapFactory.Options の outHeight, outWidth, outMimeType プロパティには読み込んだ画像の情報がセットされます。

BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filePath, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;


2. 1. の情報をもとにサブサンプルにサイズを決める

画像のサイズがわかったので、縮小して読み込むのかそのまま読み込むのかを決めます。

・そのまま読み込んだ場合に使われるメモリサイズ
・読み込む画像に割り当てたいメモリサイズ(アプリの他の要因などから決まる)
・読み込んだ画像をセットする ImageView の大きさ
・デバイスの画面サイズとピクセル密度

このあたりの要因から決めます。
よっぽど ImageView が大きくなければ、だいたいは ImageView の大きさで決めます。

デコーダーに画像をサブサンプルさせる(縮小して読み込む、低解像度で読み込む)には、BitmapFactory.Options の inSampleSize パラメータに 1 より大きい整数値を指定します。
(原文ではなぜか true をセットと書いてありますが間違いです)

inSampleSize に 1 より大きい整数値を指定すると、縦横それぞれが約 1 / inSampleSize になって読み込まれます。
例えば

元 : 2048 x 1536
|
inSampleSize = 4

Bitmap : 512 x 384

縦横がそれぞれ 1 / inSampleSize になるので、サイズは 1 / inSampleSize^2 になります。


表示するサイズから inSampleSize を求めるにはこうします。

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 画像の元サイズ final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float)height / (float)reqHeight); } else { inSampleSize = Math.round((float)width / (float)reqWidth); } } return inSampleSize; }

Math.round() だと四捨五入なので、切り上げられる場合、表示されるサイズよりも得られる Bitmap のサイズが小さくなります。それが嫌な場合は Math.floor() を使って切り下げるようにします。

inSampleSize には 2 のべき乗を指定したほうが効果的に速くデコードできますが、得られた Bitmap をメモリやディスクにキャッシュするなら、一番近いサイズになるように inSampeSize を決めたほうがいいです。

inSampleSize は 2 のべき乗にして大雑把に縮小し、そのあと Matrix を使って目的のサイズに近い Bitmap を取得する方法もあります。
createBitmap | Android Developers
Matrix | Android Developers



3. 2. で決めたサブサンプルで Bitmap をメモリに読み込む

BitmapFactory.Options の inFustDecodeBounds を false に戻して、2. で決めた値を inSampleSize にセットしてデコードします。

public static Bitmap decodeSampledBitmapFromFile(String filePath, int reqWidth, int reqHeight) { // inJustDecodeBounds=true で画像のサイズをチェック final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filePath, options); // inSampleSize を計算 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // inSampleSize をセットしてデコード options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filePath, options); }

例えば、 <resources> <dimen name="image_size">48dip</dimen> </resources> <ImageView android:id="@+id/image" android:layout_width="@dimen/image_size" android:layout_height="@dimen/image_size" />

という ImageView なら、

・View の getWidth(), getHeight() からサイズを取得
・ImageView に対して measure() を呼んで getMeasuredWidth(), getMeasuredHeight() からサイズを取得
・Resources の getDimensionPixelSize() を使って image_size の大きさを取得

という方法があります。
ImageView の大きさが固定値なら、最後の方法がいいと思います。
大きさが可変なら、最初か2番目の方法を使います。

Resources res = getResources(); int size = res.getDimensionPixelSize(R.dimen.image_size); mImageView.setImageBitmap( decodeSampledBitmapFromFile(filePath, size, size));



2012年8月21日火曜日

Support Package の LruCache を使うときの注意点

Android 3.1 (API Level 12) からキャッシュ用の便利クラスである LruCache が追加されました。

API Level 12 の方にはサンプルコードが載っています。

int cacheSize = 4 * 1024 * 1024; // 4MiB LruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }

この LruCache は Support Package にもバックポートされています。

ここで注意点なのですが、上記のコードをこっちで使う場合は

int cacheSize = 4 * 1024 * 1024; // 4MiB LruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return bitmap.getRowBytes() * bitmap.getHeight(); } }

のようにします。

実は Bitmap の getByteCount() メソッドも API Level 12 からなんです。

このメソッドの中身は次のように getRowBytes() と getHeight() の積です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java#getByteCount 828 public final int getByteCount() { 829 // int result permits bitmaps up to 46,340 x 46,340 830 return getRowBytes() * getHeight(); 831 }

わざわざ新しく API に入れるほどでもなかったのでは。。。




2012年7月26日木曜日

Android Honeycomb 以降ではダイアログの外部タッチで閉じる、がデフォルトになっている

ダイアログの周りの領域をタッチしたときにダイアログを閉じるかどうかは setCanceledOnTouchOutside() で設定できます。

Honeycomb 以降では、ダイアログの外部をタッチしたときにダイアログが閉じるようになっているので、それが嫌な場合は Dialog のインスタンスに対して setCanceledOnTouchOutside(false) を呼べば OK です。

DialogFragment なら onActivityCreated() で getDialog() で Dialog のインスタンスを取得できます。


なんでこうなってるのかを調べたら意外に深かった。。。

まず、Dialog の setCanceledOnTouchOutSide() は内部では Window に対して setCloseOnTouchOutside() を呼んでいます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Dialog.java#1059
1059 public void setCanceledOnTouchOutside(boolean cancel) { 1060 if (cancel && !mCancelable) { 1061 mCancelable = true; 1062 } 1063 1064 mWindow.setCloseOnTouchOutside(cancel); 1065 }

Dialog クラス内では setCanceledOnTouchOutside() を呼んでいません。 検索しても呼んでるのは AlertDialog.Builder、TimePickerDialog、SerchDialog のみ。

ちなみに AlertDialog.Builder では create() 時にキャンセル可能なら true をセットしています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/AlertDialog.java#917
912 public AlertDialog create() { 913 final AlertDialog dialog = new AlertDialog(P.mContext, mTheme, false); 914 P.apply(dialog.mAlert); 915 dialog.setCancelable(P.mCancelable); 916 if (P.mCancelable) { 917 dialog.setCanceledOnTouchOutside(true); 918 } 919 dialog.setOnCancelListener(P.mOnCancelListener); 920 if (P.mOnKeyListener != null) { 921 dialog.setOnKeyListener(P.mOnKeyListener); 922 } 923 return dialog; 924 }

次に Window の setCloseOnTouchOutside() を見たら

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/Window.java#setCloseOnTouchOutside 123 private boolean mCloseOnTouchOutside = false; 124 private boolean mSetCloseOnTouchOutside = false; 812 public void setCloseOnTouchOutside(boolean close) { 813 mCloseOnTouchOutside = close; 814 mSetCloseOnTouchOutside = true; 815 } 816 817 /** @hide */ 818 public void setCloseOnTouchOutsideIfNotSet(boolean close) { 819 if (!mSetCloseOnTouchOutside) { 820 mCloseOnTouchOutside = close; 821 mSetCloseOnTouchOutside = true; 822 } 823 }

mCloseOnTouchOutside に値をセットしている。この変数の初期値は false で、値をセットしているのは setCloseOnTouchOutside() と setCloseOnTouchOutsideIfNotSet() だけ、このどちらかがデフォルトで呼ばれていないと初期値が false なのでつじつまが合わない。

setCloseOnTouchOutside は Dialog と Activity からしか呼ばれていないし、Activity で呼んでいるところは関係なかった。

setCloseOnTouchOutsideIfNotSet で検索したら AlertController と PhoneWindow から呼ばれてる。 PhoneWindow を見ると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java#2567 2567 if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion 2568 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 2569 if (a.getBoolean( 2570 com.android.internal.R.styleable.Window_windowCloseOnTouchOutside, 2571 false)) { 2572 setCloseOnTouchOutsideIfNotSet(true); 2573 } 2574 }

ビンゴ。ここで HONEYCOMB がでてくる。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/values/themes.xml#174 <style name="Theme"> ... <item name="windowCloseOnTouchOutside">false</item> </style> <style name="Theme.Dialog"> ... <item name="android:windowCloseOnTouchOutside">@bool/config_closeDialogWhenTouchOutside</item> </style> <style name="Theme.Dialog.NoFrame"> ... <item name="android:windowCloseOnTouchOutside">false</item> </style> <style name="Theme.Toast" parent="@android:style/Theme.Dialog"> ... <item name="android:windowCloseOnTouchOutside">false</item> </style> http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/values/config.xml#93 <bool name="config_closeDialogWhenTouchOutside">true</bool>

なるほど。

ということは、DialogFragment でオリジナルテーマ設定してるなら

<style name="MyDialogTheme" parent="@android:style/Theme.Holo.Light.Dialog"> ... <item name="android:windowCloseOnTouchOutside">false</item> </style>

とかして

@Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new Dialog(getActivity(), R.style.MyDialogTheme); }

とすれば、このテーマを使ってるもろもろの DialogFragment を一括で変更できたー!