2013/04/09

画像の切り抜き(トリミング)なら BitmapRegionDecoder が超便利

Android で画像を読み込むのは結構厄介。一つの ImageView に読み込むならそんなに悩まないけど、 ListView やら GridView やらで画像を扱うとすぐ OutOfMemory って怒られる。限られた資源、守りたいですね。

基本は BitmapFactory.Options.inJustDecodeBounds = true にして、画像の情報だけを とって BitmapFactory.Options.inSampleSize を適切にしてあげて全体を無難に読み込む。そうじゃなくて、大きな画像の一部を切り抜いてもいいんじゃないかと。そんな画像切り抜き BitmapRegionDecoder クラスというのがあります。


とりあえず

サンプルは Github に上げた。

読み込み

画像の読み込みはバックグラウンドで行うのが基本。AsyncTaskLoader で行なっている。
// 基本は一緒
// inJustDecodeBounds = true で読み込んで画像サイズを取得する
// options.outWidth, options.outHeight にサイズが入る
inputStreamForDecodeBounds = manager.open(FILE_NAME);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStreamForDecodeBounds, null, options);
int bitmapWidth = options.outWidth;
int bitmapHeight = options.outHeight;
// 0 <= width <= bitmapWidth
// 0 <= height <= bitmapHeight
// でくり抜ける
// InputStream は使い回しできないので作り直し!!
inputStream = manager.open(FILE_NAME);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, true);
// Rect rect = new Rect(0, 0, 200, 200); // 左上から 200 * 200 で切り出す。
Rect rect = new Rect(150, 150, 400, 400); // 左上 (50, 100)から 400 * 400 で切り出す。
// 右下100 * 100 を切り出す
// Rect rect = new Rect(bitmapWidth - 200, bitmapHeight - 200, bitmapWidth,
// bitmapHeight);
// Rect rect = new Rect(0, 0, bitmapWidth, bitmapHeight); // 全読み
// inJustDecodeBounds = false; にして実際に読み込む
options.inJustDecodeBounds = false;
Bitmap bitmap = decoder.decodeRegion(rect, options);
return bitmap;
view raw gistfile1.java hosted with ❤ by GitHub

例では assets にある画像ファイルを読み込んで表示しようとしています。読み込んだ画像よりも Rect のサイズが大きいと例外になると思いきや、はみ出した部分は真っ黒で塗りつぶされた画像が出来上がる。綺麗にやろうとしたらちゃんとサイズを見たほうがいいと思う。

そもそも画像をくり抜こうとしたら Bitmap  作って そこから Canvas 作って Rect 作ってと・・・意外にめんどい。しかも メモリ節約のために小さく切り抜くのにあたらに Bitmap 作るなんて本末転倒。そこはさくっと BitmapRegionDecoder で仕上げるのが吉。

ただし、BitmapRegionDecoder は API Level 10 以上。Froyo[1] Gingerbread (API Level 9,  < Android OS 2.3.3) には対応していません。Android OS 1.6, 2.2 以上がターゲットのアプリには使えないことになります。 まあ、今なら Gingerbread (API Level 10,  >= Android OS 2.3.3)もしくは ICS 以降をターゲットにするでしょうから、全く問題ない範囲だと思います。

無駄にハマったこと

BitmapFactory.Options.inJustDecodeBounds = true で必要な InputStream と BitmapFactory.Options.inJustDecodeBounds = false で実際に読み込む時の InputStream。同じ物を使いまわすとデコードできません。BitmapRegionDecoder#decodeRegion() の場合は
java.io.IOException: Image format not supported
の IOException が出ます。IOException でそのメッセージですか、そうですか・・・。InputStream は別個に作成しましょう。


修正

[1] API Level 9 は初期 Gingerbread でした。G+ にてご指摘頂きまして、ありがとうございます。