Cómo cambiar el tamaño de las imágenes para un mejor rendimiento de carga / descarga. Desarrollo de Android.

No es muy común ver un proyecto que no requiere la carga de fotos de una forma u otra. La mayoría de los dispositivos Android de gama alta hoy crean una foto tan grande como 2 MB. Esto es un problema, ¿cómo? Imagine que está creando una aplicación móvil que requiere que sus usuarios tengan una foto de perfil, teniendo en cuenta la escalabilidad, cada foto no debe ser> 100 KB, ¿estoy en lo cierto? No desea guardar una foto de 1 MB para cada usuario. No solo afectará su UX, su servidor también sufriría. Cargar fotos grandes en un servidor remoto puede causar uno o todos los siguientes problemas

  • Operación lenta de carga / descarga en dispositivos con bajo ancho de banda de red
  • Afecta la escalabilidad del producto
  • Y más…

SOLUCIÓN

La verdadera solución obvia es cambiar el tamaño de la foto antes de enviarla al servidor remoto. ¡Bueno!. ¿Pero cómo? Eso es lo que estoy tratando de mostrar.

Tuve pocos problemas para descubrir cómo resolvería esto cuando comencé a escribir aplicaciones para Android, así que decidí compartirlo hoy. Espero ver a alguien que me muestre una mejor manera de hacerlo. ¡Vamonos!

Suposición

  • Supongo que es conveniente con el lenguaje de programación Java (eso es lo que usaré, puede obtenerlo fácilmente si también tiene experiencia con otros lenguajes de programación).
  • Tenga Android Studio y SDK instalados y listos.
  • Crea un nuevo proyecto de estudio de Android. Yo llamo al mío EasyPhotoUpload

Nombre del proyecto - EasyPhotoUpload

SDK mínimo - 4.0.3, API 15

Básicamente, estamos tratando de

  • Permitir al usuario seleccionar una foto de la Galería
  • Obtener la ruta de la foto seleccionada
  • Cambie el tamaño de la foto seleccionada en un hilo de fondo y devuelva el resultado al hilo principal: como verá, cambiar el tamaño de la imagen es una operación costosa y no debe realizarse en el hilo principal

El código final para este artículo está disponible en github: https://github.com/adigunhammedolalekan/easyphotoupload

Una vez que Android Studio termine de construir el proyecto y esté listo, cree estos paquetes: núcleo, oyentes, utilidades

En el paquete util, cree una nueva clase, Util.java, el siguiente es el contenido del archivo.

‘Util de clase pública {

// SDF para generar un nombre único para el archivo comprimido.
 public static final SimpleDateFormat SDF = new SimpleDateFormat ("aaaammddhhmmss", Locale.getDefault ());

/ *
 comprima el archivo / foto desde @param ruta a una ubicación privada en el dispositivo actual y devuelva el archivo comprimido.
 @param path = La ruta de la imagen original
 @param context = Contexto actual de Android
 * /
 El archivo estático público getCompressed (contexto de contexto, ruta de cadena) genera IOException {

if (contexto == nulo)
 lanzar una nueva NullPointerException ("El contexto no debe ser nulo");
 // obtener el directorio de caché externo del dispositivo, puede que no esté disponible en algunos dispositivos,
 // entonces nuestro código vuelve al directorio de caché de almacenamiento interno, que siempre está disponible pero en menor cantidad
 Archivo cacheDir = context.getExternalCacheDir ();
 if (cacheDir == nulo)
 //retroceder
 cacheDir = context.getCacheDir ();

Cadena rootDir = cacheDir.getAbsolutePath () + "/ ImageCompressor";
 Archivo raíz = archivo nuevo (rootDir);

// Cree la carpeta ImageCompressor si aún no existe.
 if (! root.exists ())
 root.mkdirs ();

// decodifica y redimensiona el mapa de bits original desde la ruta @param.
 Mapa de bits mapa de bits = decodeImageFromFiles (ruta, / * su ancho deseado * / 300, / * su altura deseada * / 300);

// crea marcador de posición para el archivo de imagen comprimido
 Archivo comprimido = nuevo Archivo (raíz, SDF.formato (nueva Fecha ()) + “.jpg” / * Su formato deseado * /);

// convierte el mapa de bits decodificado para transmitir
 ByteArrayOutputStream byteArrayOutputStream = nuevo ByteArrayOutputStream ();

/ * comprimir mapa de bits en byteArrayOutputStream
 Bitmap.compress (Formato, Calidad, OutputStream)

Donde la calidad varía de 1 a 100.
 * /
 bitmap.compress (Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);

/ *
 En este momento, tenemos nuestro mapa de bits dentro del objeto byteArrayOutputStream, todo lo que necesitamos a continuación es escribirlo en el archivo comprimido que creamos anteriormente,
 java.io.FileOutputStream puede ayudarnos a hacer eso.

* /
 FileOutputStream fileOutputStream = nuevo FileOutputStream (comprimido);
 fileOutputStream.write (byteArrayOutputStream.toByteArray ());
 fileOutputStream.flush ();

fileOutputStream.close ();

// Archivo escrito, volver a la persona que llama. ¡Hecho!
 volver comprimido;
 }

Public static Bitmap decodeImageFromFiles (Ruta de la cadena, int ancho, int alto) {
 BitmapFactory.Options scaleOptions = new BitmapFactory.Options ();
 scaleOptions.inJustDecodeBounds = true;
 BitmapFactory.decodeFile (ruta, scaleOptions);
 escala int = 1;
 while (scaleOptions.outWidth / scale / 2> = ancho
 && scaleOptions.outHeight / scale / 2> = height) {
 escala * = 2;
 }
 // decodifica con el tamaño de la muestra
 BitmapFactory.Options outOptions = new BitmapFactory.Options ();
 outOptions.inSampleSize = scale;
 return BitmapFactory.decodeFile (ruta, outOptions);
 }
} ’

El método que maneja la compresión y el almacenamiento de las fotos es 'Archivo estático getCompressed (Context, String)', como ha visto en el código anterior, este método toma una ruta a una foto existente en el dispositivo, cambia el tamaño y la almacena en un ubicación privada en el dispositivo y devuelve el archivo recién comprimido. Voila!

El siguiente archivo que examinaremos se llama ImageCompressTask.java, esta clase implementa un Runnable, con un constructor de tres argumentos y en su método run (), la compresión ocurre todo en el hilo de fondo, luego publica el resultado final en el main hilo con la ayuda de android.os.Handler o informar el error de lo contrario.

ImageCompressTask.java

‘Public class ImageCompressTask implementa Runnable {

Contexto privado mContext;
 Lista privada originalPaths = new ArrayList <> ();
 controlador privado mHandler = nuevo controlador (Looper.getMainLooper ());
 Lista privada resultado = nueva ArrayList <> ();
 IImageCompressTaskListener privado mIImageCompressTaskListener;

public ImageCompressTask (contexto de contexto, ruta de cadena, IImageCompressTaskListener compressTaskListener) {

originalPaths.add (ruta);
 mContext = contexto;

mIImageCompressTaskListener = compressTaskListener;
 }
 public ImageCompressTask (contexto de contexto, rutas de List , IImageCompressTaskListener compressTaskListener) {
 originalPaths = caminos;
 mContext = contexto;
 mIImageCompressTaskListener = compressTaskListener;
 }
 @Anular
 public void run () {

tratar {

// Recorre todas las rutas dadas y recoge el archivo comprimido de Util.getCompressed (Context, String)
 for (Ruta de la cadena: originalPaths) {
 Archivo de archivo = Util.getCompressed (mContext, ruta);
 // ¡agrégalo!
 result.add (archivo);
 }
 // usa Handler para publicar el resultado en el hilo principal
 mHandler.post (nuevo Runnable () {
 @Anular
 public void run () {

if (mIImageCompressTaskListener! = nulo)
 mIImageCompressTaskListener.onComplete (resultado);
 }
 });
 } catch (final IOException ex) {
 // Hubo un error, informe el error nuevamente a través de la devolución de llamada
 mHandler.post (nuevo Runnable () {
 @Anular
 public void run () {
 if (mIImageCompressTaskListener! = nulo)
 mIImageCompressTaskListener.onError (ex);
 }
 });
 }
 }
} ’

Finalmente, cree MainActivity.java, la interfaz de usuario para toda la aplicación de muestra.

MainActivity.java


clase pública MainActivity extiende AppCompatActivity {

Botón selectImage;
 ImageView selectedImage;

private static final int REQUEST_STORAGE_PERMISSION = 100;
 privado estático final int REQUEST_PICK_PHOTO = 101;

// crea un grupo de subprocesos único para nuestra clase de compresión de imagen.
 Private ExecutorService mExecutorService = Executors.newFixedThreadPool (1);

ImageCompressTask privado imageCompressTask;

@Anular
 vacío protegido onCreate (Bundle savedInstanceState) {
 super.onCreate (savedInstanceState);
 setContentView (R.layout.activity_main);

selectedImage = (ImageView) findViewById (R.id.iv_selected_photo);
 selectImage = (Botón) findViewById (R.id.btn_select_image);

selectImage.setOnClickListener (nueva View.OnClickListener () {
 @Anular
 public void onClick (Ver vista) {
 solicitar permiso();
 }
 });
 }

void requestPermission () {

if (PackageManager.PERMISSION_GRANTED! =
 ContextCompat.checkSelfPermission (this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
 if (ActivityCompat.shouldShowRequestPermissionRationale (this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
 ActivityCompat.requestPermissions (esto, nueva Cadena [] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
 REQUEST_STORAGE_PERMISSION);
 } más {
 //¡Sí! Quiero que ambos bloques hagan lo mismo, puedes escribir tu propia lógica, pero esto funciona para mí.
 ActivityCompat.requestPermissions (esto, nueva Cadena [] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
 REQUEST_STORAGE_PERMISSION);
 }
 } más {
 // Permiso concedido, vamos a elegir la foto
Intención intención = nueva intención (Intent.ACTION_PICK);
intent.setAction (Intent.ACTION_GET_CONTENT);
intent.setType ("imagen / *");
startActivityForResult (intento, REQUEST_PICK_PHOTO);
 }

}

@Anular
 void protegido en OnActivityResult (int requestCode, int resultCode, Intent data) {
 super.onActivityResult (requestCode, resultCode, data);
 if (requestCode == REQUEST_PICK_PHOTO && resultCode == RESULT_OK &&
 datos! = nulo) {
 // extraer ruta de imagen absoluta de Uri
 Uri uri = data.getData ();
 Cursor cursor = MediaStore.Images.Media.query (getContentResolver (), uri, nueva cadena [] {MediaStore.Images.Media.DATA});
 if (cursor! = null) {
 String path = cursor.getString (cursor.getColumnIndexOrThrow (MediaStore.Images.Media.DATA));

// Crea ImageCompressTask y ejecuta con Executor.
 imageCompressTask = new ImageCompressTask (this, ruta, iImageCompressTaskListener);

mExecutorService.execute (imageCompressTask);
 }
 }
 }

// devolución de llamada de tarea de compresión de imagen
 IImageCompressTaskListener privado iImageCompressTaskListener = new IImageCompressTaskListener () {
 @Anular
 public void onComplete (List comprimido) {
 // foto comprimida. ¡Hurra!

// prepararse para las cargas.

Archivo de archivo = comprimido.get (0);

selectedImage.setImageBitmap (BitmapFactory.decodeFile (file.getAbsolutePath ()));
 }

@Anular
 public void onError (error de lanzamiento) {
 // muy poco probable, pero podría suceder en un dispositivo con almacenamiento extremadamente bajo.
 // regístralo, log.WhatTheFuck ?, o muestra un cuadro de diálogo pidiéndole al usuario que elimine algunos archivos… .etc, etc.
 Log.wtf ("ImageCompressor", "Error ocurrido", error);
 }
 };

@Anular
 nulo protegido en Destroy () {
 super.onDestroy ();

//¡limpiar!
 mExecutorService.shutdown ();

mExecutorService = nulo;
 imageCompressTask = nulo;
 }
} ’

Todos los códigos están bien comentados, pero si tiene problemas con alguna parte. ¡Déjame saber! Todos los códigos están en github, visítelo para verlo mejor. La captura de pantalla del resultado final.

Me encantará tu contribución a esto. ¡Gracias!

Sobre mi

Soy un desarrollador apasionado de aplicaciones móviles con más de 2.5 experiencia en la construcción de excelentes aplicaciones para la plataforma Android. Si necesitas mi talento, ¡no dudes en contactarme!

Github - www.github.com/adigunhammedolalekan

Correo - adigunhammed.lekan@gmail.com

Contacto - 07035452307