注: 画像の読み込みに関するおすすめの方法に基づくライブラリはいくつかあります。アプリでこれらのライブラリを使用すると、最適な方法で画像を読み込むことができます。おすすめのライブラリである Glide を使用すると、画像の読み込みと表示をできる限り迅速かつスムーズに行うことができます。画像読み込み用のライブラリとして、Square の Picasso、Instacart のCoil、Facebook の Fresco なども人気があります。これらのライブラリを使用すると、Android でビットマップなどの画像に関連付けられている複雑なタスクのほとんどを簡素化できます。
画像の形状やサイズはそれぞれ異なります。多くの場合、画像のサイズは一般的なアプリのユーザー インターフェース(UI)に必要なサイズより大きくなります。たとえば、システムのギャラリー アプリでは、Android デバイスのカメラを使って撮影した写真が表示されますが、通常、それらの写真の解像度はデバイスの画面密度よりはるかに高くなります。
メモリに限りがある場合、理想的には、解像度の低い画像のみをメモリに読み込みたいところです。解像度の低い画像は、それを表示する UI コンポーネントのサイズと一致する必要があります。解像度が高い画像は、表示上のメリットがないだけでなく、貴重なメモリも消費します。また、瞬時に追加のサイズ調整を行うため、パフォーマンスのオーバーヘッドがさらに発生します。
このレッスンでは、サブサンプリングされた縮小画像をメモリに読み込むことで、アプリごとのメモリの制限を超えずに大容量のビットマップをデコードする方法について説明します。
ビットマップのディメンションとタイプを読み取る
BitmapFactory
クラスには、さまざまなソースから Bitmap
を作成するための複数のデコード メソッド(decodeByteArray()
、decodeFile()
、decodeResource()
など)が用意されています。画像のデータソースに基づいて、最も適切なデコード メソッドを選択してください。これらのメソッドは作成済みのビットマップに対してメモリを割り当てようとするため、OutOfMemory
例外が頻繁に発生します。各タイプのデコード メソッドには、BitmapFactory.Options
クラスを介してデコード オプションを指定できる追加のシグネチャがあります。デコード中に inJustDecodeBounds
プロパティを true
に設定すると、メモリの割り当てを回避できます。その結果、ビットマップ オブジェクト用に null
が返されますが、outWidth
、outHeight
、outMimeType
が設定されます。この手法により、ビットマップの作成(およびメモリの割り当て)が行われる前に画像データのディメンションとタイプを読み取ることができます。
Kotlin
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeResource(resources, R.id.myimage, options) val imageHeight: Int = options.outHeight val imageWidth: Int = options.outWidth val imageType: String = options.outMimeType
Java
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;
java.lang.OutOfMemory
例外が発生しないようにするには、使用可能なメモリ内に容易に収まる予測可能なサイズの画像データの提供元を絶対的に信頼できる場合を除き、デコードする前にビットマップのディメンションを確認します。
縮小画像をメモリに読み込む
画像のディメンションがわかったので、そのディメンションを使用して、画像全体をメモリに読み込むか、サブサンプリングされた画像を読み込むかを決定します。考慮すべき要素は次のとおりです。
- 画像全体をメモリに読み込む場合の推定メモリ使用量。
- 他にアプリのメモリ要件がある場合に、画像の読み込みに使用するメモリの量。
- 画像の読み込み先の
ImageView
または UI コンポーネントのディメンション。 - 現在のデバイスの画面のサイズと密度。
たとえば、1024x768 ピクセルの画像を最終的に ImageView
内の 128x96 ピクセルのサムネイルに表示する場合、その画像をメモリに読み込む価値はありません。
画像をサブサンプリングするようデコーダに指示するには、縮小画像をメモリに読み込んで、BitmapFactory.Options
オブジェクトで inSampleSize
を true
に設定します。たとえば、inSampleSize
を 4 に設定し、解像度が 2048x1536 の画像をデコードすると、約 512x384 のビットマップが生成されます。このビットマップをメモリに読み込む場合、画像全体で 12 MB ではなく 0.75 MB を使用します(ビットマップの設定を ARGB_8888
と仮定)。以下に、サンプルサイズの値(ターゲットの幅と高さに基づく 2 のべき乗の値)を計算するメソッドを示します。
Kotlin
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { // Raw height and width of image val (height: Int, width: Int) = options.run { outHeight to outWidth } var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight: Int = height / 2 val halfWidth: Int = width / 2 // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize }
Java
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
注: 2 のべき乗の値を算出する理由は、inSampleSize
のドキュメントに記載されているように、最も近い 2 のべき乗の値に切り捨てることによって決定された値がデコーダで使用されるためです。
このメソッドを使用するには、まず inJustDecodeBounds
を true
に設定してデコードし、オプションを渡します。
新しい inSampleSize
値と false
に設定された inJustDecodeBounds
を使用して再度デコードします。
Kotlin
fun decodeSampledBitmapFromResource( res: Resources, resId: Int, reqWidth: Int, reqHeight: Int ): Bitmap { // First decode with inJustDecodeBounds=true to check dimensions return BitmapFactory.Options().run { inJustDecodeBounds = true BitmapFactory.decodeResource(res, resId, this) // Calculate inSampleSize inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) // Decode bitmap with inSampleSize set inJustDecodeBounds = false BitmapFactory.decodeResource(res, resId, this) } }
Java
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
このメソッドを使用すると、次のサンプルコードに示すように、任意の大きなサイズのビットマップを、100x100 ピクセルのサムネイルを表示する ImageView
に簡単に読み込むことができます。
Kotlin
imageView.setImageBitmap( decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100) )
Java
imageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
必要に応じて適切な BitmapFactory.decode*
メソッドを代用することで、同様のプロセスに従って他のソースからビットマップをデコードすることができます。