Nota: Existen varias bibliotecas que siguen las prácticas recomendadas para cargar imágenes. Puedes usar estas bibliotecas en tu app para cargar imágenes de manera optimizada. Te recomendamos la biblioteca Glide, que carga y muestra imágenes de la manera más rápida y fluida posible. Otras bibliotecas populares de carga de imágenes son Picasso, de Square, Coil, de Instacart, y Fresco, de Facebook. Estas bibliotecas simplifican la mayoría de las tareas complejas asociadas con mapas de bits y otros tipos de imágenes en Android.
Las imágenes pueden ser de diversas formas y tamaños. En muchos casos, son más grandes de lo necesario para una interfaz de usuario (IU) de aplicación típica. Por ejemplo, la aplicación de la galería del sistema muestra fotos tomadas con la cámara de los dispositivos Android que generalmente tienen una resolución mucho más alta que la densidad de la pantalla del dispositivo.
Dado que estás trabajando con memoria limitada, idealmente solo debes subir una versión de menor resolución a la memoria. La versión de menor resolución debe coincidir con el tamaño del componente de la IU que la muestra. Una imagen con una resolución más alta no proporciona ningún beneficio visible, pero aún ocupa memoria valiosa e incurre en sobrecarga de rendimiento adicional debido a un escalamiento adicional en el momento.
En esta lección, se explica cómo decodificar mapas de bits grandes sin exceder el límite de memoria por aplicación cuando se sube una versión de submuestreo más pequeña a la memoria.
Cómo leer las dimensiones y el tipo del mapa de bits
La clase BitmapFactory
proporciona varios métodos de decodificación (decodeByteArray()
, decodeFile()
, decodeResource()
, etc.) para crear un Bitmap
de diferentes fuentes. Elige el método de decodificación más adecuado según tu fuente de datos de imágenes. Estos métodos intentan asignar memoria para el mapa de bits construido y, por lo tanto, pueden generar fácilmente una excepción OutOfMemory
. Cada tipo de método de decodificación tiene firmas adicionales que te permiten especificar opciones de decodificación a través de la clase BitmapFactory.Options
. Al establecer la propiedad inJustDecodeBounds
en true
durante la decodificación, se evita la asignación de memoria, con lo cual se muestra null
para el objeto de mapa de bits, pero se establecen outWidth
, outHeight
y outMimeType
. Esta técnica permite leer las dimensiones y el tipo de los datos de la imagen antes de la construcción (y la asignación de memoria) del mapa de bits.
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;
Para evitar las excepciones java.lang.OutOfMemory
, comprueba las dimensiones de un mapa de bits antes de decodificarlo, a menos que confíes absolutamente en que la fuente te proporcionará datos de imagen de tamaño predecible que se ajusten cómodamente a la memoria disponible.
Cómo subir una versión reducida a la memoria
Ahora que se conocen las dimensiones de la imagen, se pueden usar para decidir si la imagen completa se debe subir en la memoria o si se debe subir una versión de submuestreo. Estos son algunos factores para considerar:
- Uso de memoria estimado cuando se sube la imagen completa a la memoria
- Cantidad de memoria que quieres comprometer para cargar esta imagen, dado cualquier otro requisito de memoria de tu aplicación
- Dimensiones del componente
ImageView
o de IU objetivo en el que se debe cargar la imagen - Tamaño de pantalla y densidad del dispositivo actual
Por ejemplo, no vale la pena cargar una imagen de 1024 x 768 píxeles en la memoria si finalmente se mostrará en una miniatura de 128 x 96 píxeles en ImageView
.
Para indicar al decodificador que debe realizar un submuestreo de la imagen y subir una versión más pequeña a la memoria, establece inSampleSize
como true
en tu objeto BitmapFactory.Options
. Por ejemplo, una imagen con una resolución de 2048 x 1536 que se decodifica con un inSampleSize
de 4 produce un mapa de bits de aproximadamente 512 x 384. Cuando se carga esto en la memoria, se utilizan 0.75 MB en lugar de 12 MB para la imagen completa (suponiendo una configuración de mapa de bits de ARGB_8888
). Aquí se muestra un método para calcular un valor de tamaño de muestra con la potencia de dos en función del ancho y la altura del objetivo:
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; }
Nota: Se calcula una potencia de dos valores porque el decodificador utiliza un valor final redondeando a la potencia de dos valores más cercana y más baja, según la documentación de inSampleSize
.
Para usar este método, primero decodifica con inJustDecodeBounds
establecido en true
, pasa las opciones y, luego, decodifica de nuevo utilizando el nuevo valor inSampleSize
y inJustDecodeBounds
establecido en false
:
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); }
Este método facilita la carga de un mapa de bits de tamaño arbitrariamente grande en un ImageView
que muestra una miniatura de 100 x 100 píxeles, como se muestra en el siguiente código de ejemplo:
Kotlin
imageView.setImageBitmap( decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100) )
Java
imageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
Para seguir un proceso similar para decodificar mapas de bits de otras fuentes, puedes sustituir el método apropiado de BitmapFactory.decode*
según sea necesario.