読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Android の Bitmap と BitmapDrawable はそれぞれ密度 (density) を保持している

Android アプリ

Android アプリ開発時の画面密度とビットマップ画像に関する話です。

前提知識

画面密度 (Screen density)
画面上の実際のサイズ中におけるピクセル量; 通常は dpi (dot-per-inch) と呼ばれる。 例えば “low” 密度の画面は、“normal” や “high” 密度の画面と比べ、同じ領域中に含まれるピクセル量は少ない。 簡単のため、Android では画面密度は次の 4 つのグループに分けられている: low、medium、high、extra high。
密度非依存のピクセル (Density-independent pixel; dp)
UI レイアウトを行う際に、画面密度に依存しないようにするために使われる仮想的なピクセルの単位。 1 dp は、画面密度が 160 dpi である画面上の実際の 1 px と同等の大きさである。 実行時には、システムは必要に応じて、使用中の画面の実際の密度に基づいて dp 単位の拡大縮小を処理する。 dp 単位を画面上のピクセルに変換するのは次の通り: px = dp * (dpi / 160)。 例えば、240 dpi の画面上では、1 dp は物理的な 1.5 px と等しい。 様々な密度の画面上で、想定した UI の表示が行われるように、常に dp 単位を使用すべきである。

Supporting Multiple Screens | Android Developers より

Bitmap オブジェクトはそのビットマップ画像の密度を保持している

例えば画面上に幅 40 dp の大きさでビットマップ画像を表示したい場合、dot-by-dot で表示するならば、

  • 160 dpi の画面用には幅 40 px のビットマップ画像を、
  • 240 dpi の画面用には幅 60 px のビットマップ画像を、
  • また別の密度の画面用には、それに応じたピクセル数のビットマップ画像を

それぞれ用意する必要があります *1。 逆にいえば、幅 40 px のビットマップ画像があったとしても、そのビットマップ画像の密度がわからなければ、画面に表示するときにスケーリングさせる必要があるのかどうかや、スケーリングさせる必要がある場合にその拡大率がわかりません。

そこで、Android では Bitmap オブジェクトに、そのビットマップ画像の密度を保持させるようになっているようです。

Drawable リソースとしてビットマップを扱う場合は密度の計算などは自動でやってくれるので気にする必要はないのですが、HTTP 通信でビットマップ画像を取得した場合などは、密度を設定してやらないと想像以上に画像が小さく表示されてしまったりするので注意が必要です。

// 適当なリソースから Bitmap を生成する (ic_launcher は Android Studio でプロジェクトを作成したらもともと入ってるはずの drawable リソース)
Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);

// Nexus 7 (2012 年) なので 213 dpi
System.out.println("bitmap density: " + b.getDensity()); //=> 213
System.out.println("bitmap width: " + b.getWidth()); //=> 64
// 213 dpi で 64 px の大きさは、medium な密度 (160 dpi) では 48 px に相当
System.out.println("bitmap scaled width for medium density: " + b.getScaledWidth(DisplayMetrics.DENSITY_MEDIUM)); //=> 48

// Bitmap の密度は変更することができる
b.setDensity(DisplayMetrics.DENSITY_MEDIUM);
System.out.println("bitmap density: " + b.getDensity()); //=> 160
// 密度を変えても Bitmap の width は変わらない
System.out.println("bitmap width: " + b.getWidth()); //=> 64
// 160 dpi で 64 px の大きさなので、medium な密度 (160 dpi) では当然 64 px
System.out.println("bitmap scaled width for medium density: " + b.getScaledWidth(DisplayMetrics.DENSITY_MEDIUM)); //=> 64

InputStream とか byte[] などから Bitmap を生成する場合

HTTP 通信をして画像を GET して、そのレスポンスボディから Bitmap オブジェクトを生成する場合など。

// バイト列から画像を生成する。 bitmapDat は byte[] 型
Bitmap b = BitmapFactory.decodeByteArray(bitmapDat, 0, bitmapDat.length);
System.out.println(b.getDensity()); //=> 213 (使用中の端末の画面密度になる?)
// 基本的には medium な密度用の画像のはずなので、DENSITY_MEDIUM を setDensity してやればよい?
b.setDensity(DisplayMetrics.DENSITY_MEDIUM);

BitmapFactory.decodeByteArray メソッド などで Bitmap オブジェクトを生成してやると、特に指定がない場合は Bitmap の密度は、使用している端末の画面密度になるようです。 そのままだと、画面密度によって表示される画像のサイズが違ってきてしまうので、setDensity で適当な密度を設定してやる必要があるのではないかなーと思います。 (が、ここら辺はよくわかってません。)

BitmapDrawable は対象画面密度を保持している

Bitmap が密度を保持しているのと同じように、BitmapDrawable は 「どの画面密度に表示する用なのか」 という情報を保持するようになっています。 その値を外から見ることはできないようなのですが、BitmapDrawable#setTargetDensity メソッド を使って設定することができます。

BitmapDrawable の対象画面密度を変更すると、BitmapDrawable#getIntrinsicWidth メソッドBitmapDrawable#getIntrinsicHeight メソッド の返り値が変化します。 これらのメソッドは、ビットマップ画像を本来の大きさ (dp 単位) で BitmapDrawable の対象画面密度に表示するための幅と高さをピクセル単位で返してくれるというものです。 *2

// バイト列から画像を生成する。 bitmapDat は byte[] 型
Bitmap b = BitmapFactory.decodeByteArray(bitmapDat, 0, bitmapDat.length);
BitmapDrawable bd;

// 使用している端末が 213 dpi なので、両方とも 213 dpi (端末ごとによって値は違うはず)
// Bitmap: 213 dpi, BitmapDrawable: 213 dpi
bd = new BitmapDrawable(getResources(), b);
System.out.println("bitmap width: " + b.getWidth()); //=> 80
System.out.println("drawable intrinsic width: " + bd.getIntrinsicWidth()); //=> 80

// Bitmap: medium density (160 dpi), BitmapDrawable: 213 dpi
b.setDensity(DisplayMetrics.DENSITY_MEDIUM);
bd = new BitmapDrawable(getResources(), b);
System.out.println("bitmap width: " + b.getWidth()); //=> 80 (Bitmap 自体の width は変わらない)
// 160 dpi の画面で幅 80 px の画像を同じ dp サイズで 213 dpi の画面に表示するには 107 px
// (80 px / 160 dpi * 213 dpi = 106.5 px)
System.out.println("drawable intrinsic width: " + bd.getIntrinsicWidth()); //=> 107

// Bitmap: medium density (160 dpi), BitmapDrawable: high density (240 dpi)
bd = new BitmapDrawable(getResources(), b);
bd.setTargetDensity(DisplayMetrics.DENSITY_HIGH);
System.out.println("bitmap width: " + b.getWidth()); //=> 80 (Bitmap 自体の width は変わらない)
// 160 dpi の画面で幅 80 px の画像を同じ dp サイズで 240 dpi の画面に表示するには 120 px
// (80 px / 160 dpi * 240 dpi = 120 px)
System.out.println("drawable intrinsic width: " + bd.getIntrinsicWidth()); //=> 120

まとめ

Android アプリを開発するときにビットマップ画像を扱う場合は

  • ビットマップ画像自体の密度 (Bitmap オブジェクトの setDensity メソッドで設定できる)
  • BitmapDrawable の対象画面密度 (BitmapDrawable オブジェクトの setTargetDensity メソッドで設定できる)

の両方に気を配る必要があります!

*1:もちろん、実際には dot-by-dot で表示せずに、拡大縮小して表示させることもできます。

*2:これらのメソッドの返り値の値は、Bitmap の密度と BitmapDrawable の対象画面密度によって変化します。