Ratio: 0 / 5

Inicio desactivadoInicio desactivadoInicio desactivadoInicio desactivadoInicio desactivado
 

Nos queda por resolver el tema de las imágenes.

 

Artículo anterior: Android: Listview con búsqueda con SearchView + PHP + MySQL + Imágenes + Cache (parte 3)

Junto con los datos de cada elemento de nuestra carta, devolvemos una url que es simplemente un link a una imagen. Debemos implementar un método para mostrar la imagen en cada item de nuestro listview, sin que interfiera con el uso de nuestra app, y que hacer en caso de que se rompa el link, o de timeout la descarga.

Paralelamente, implementaremos una cache primitiva para las imágenes. Porqué?

Primero, alivia mucho la carga que ejerce nuestra app en el dispositivo del usuario. Por ejemplo, al abrir nuestra app, trae todos los datos y comienza la descarga de todas las imágenes. Si filtra por "Pastas", vuelve a traer todas las imágenes de pastas. Si el usuario vuelve a "Todos", otra vez baja todas las imágenes. Todo esto provoca tráfico de datos para bajar contínamente datos que generalmente no cambian demasiado.

Segundo, toda la carga anteriormente descripta, se replica en carga en nuestro server. El ícono del plato de ñoquis o del vino de "Bodegas Vieja Vejiga", lo más probable es que no cambie en el tiempo. Es mucho más eficiente bajarlo la primera vez, y cuando sea necesario, leerlo directamente desde la SD Card que bajarlo de internet. Si tenemos muchos usuarios concurrente, se puede transformar en un real problema.

El primer paso, es confirgurar el permiso de escritura en la SD Card en AndroidManifest.xml

...
...
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
...

La primer clase que crearemos será Utils.java, que utilizaremos en el proceso de creación de la cache de las imágenes bajadas desde nuestro sitio.

public class Utils {
   public static void CopyStream(InputStream is, OutputStream os) {
      final int buffer_size=1024;
      try {
         byte[] bytes=new byte[buffer_size];
         for(;;) {
            //Leer un byte desde input stream
            int count=is.read(bytes, 0, buffer_size);
            if(count==-1) break;
            //Escribir ese byte en el output stream
            os.write(bytes, 0, count);
         }
      } catch(Exception ex){
      }
   }
}

Continuamos con la clase se encargará de de limitar la cantidad de espacio que uilizaremos en la SD para el almacenamiento de imágenes. También se encargará del borrado de la cache. La llamaremos MemoriaCache.java

Rápida descripción del concepto de funcionamiento

Es una pila donde el último bitmap que se accede está al tope.

Si se requiere una imagen, se verifica si el id de ese bitmap se encuentra actualemtente en la SD. Si no se encuentra, se agrega al tope de la pila. Si se encuentra, de borra de donde esté en la pila y se agrega al tope de la misma. De esta manera, los archivos bajados y que nunca más se accedieron (o que se accedieron muy poco) se encuentran al fondo de la pila.

Cuando se supera el tamaño máximo asignado para la cache, se comienzan a borrar los archivos del fondo de la pila (los menos accedidos) hasta que el total de la cache sea menor al límite establecido.

public class MemoriaCache {
   private static final String TAG = "MemoriaCache";
   //El último argumento es true, a los efectos del ordenado LRU
   private Map<String, Bitmap> cache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(10,1.5f,true));
   //El tamaño actual de la cache
   private long size=0;
   //El tamaño máximo que ocupará en la SD nuestra cache
   private long limit=1000000;
   public MemoriaCache(){
      //utilizar el 25% del tamaño de la pila disponible
      setLimit(Runtime.getRuntime().maxMemory()/4);
   }
   public void setLimit(long new_limit){
      limit=new_limit;
      Log.i(TAG, "MemoriaCache usará hasta "+limit/1024./1024.+"MB en la SD");
   }
   //Lo usamos para obtener la imagen desde la cache
   public Bitmap get(String id){
      try{
         //Verificamos si existe el id de la imagen en la cache
         //Si no la bajamos previamente, devolver null
         if(!cache.containsKey(id))
            return null;
         //Si ya la salvamos, devolvemos la imagen
         return cache.get(id);
      }catch(NullPointerException ex){
         ex.printStackTrace();
         return null;
      }
   }
   //LO usamos para notificar que colocamos un archivo en la cache
   public void put(String id, Bitmap bitmap){
      try{
         //Verificamos si existe el id en la cache
         //Si existe, borramos el espacio informado
         if(cache.containsKey(id))
            size-=getSizeInBytes(cache.get(id));
         //Y agregamos el ítem al tope de la lista
         cache.put(id, bitmap);
         size+=getSizeInBytes(bitmap);
         checkSize();
      }catch(Throwable th){
         th.printStackTrace();
      }
   }
   private void checkSize() {
      Log.i(TAG, "Tamaño de la cache="+size+" length="+cache.size());
      //Verificamos si el tamaño actual es mayor que el límite
      if(size>limit){
         // El item menos accedido, se borra
         Iterator<Entry<String, Bitmap>> iter=cache.entrySet().iterator();
         while(iter.hasNext()){
            Entry<String, Bitmap> entry=iter.next();
            size-=getSizeInBytes(entry.getValue());
            iter.remove();
            if(size<=limit) break;
         }
         Log.i(TAG, "Se limpió la cache. El nuevo tamaño es "+cache.size());
      }
   }
   public void clear() {
      try{
         // Borrar cache
         cache.clear();
         size=0;
      }catch(NullPointerException ex){
         ex.printStackTrace();
      }
   }
   long getSizeInBytes(Bitmap bitmap) {
      if(bitmap==null) return 0;
      return bitmap.getRowBytes() * bitmap.getHeight();
   }
}

La próxima clase a crear es la que se encargará de crear en la SD el directorio para la cache. Se llamará ArchivoCache.java

public class ArchivoCache {
   private File cacheDir;
   public ArchivoCache(Context context) {
      // Buscar el directorio en la SD para salvar las imágenes
      if (android.os.Environment.getExternalStorageState().equals( android.os.Environment.MEDIA_MOUNTED)) {
         // Si la SD está montada
         cacheDir = new File( android.os.Environment.getExternalStorageDirectory(), "PHPMySQLListView2");
      } else {
         // Si no está montada, crear el directorio en el contexto
         // de nuesta app
         cacheDir = context.getCacheDir();
      }
      if (!cacheDir.exists()) {
         // Crear el directorio
         cacheDir.mkdirs();
      }
   }
   public File getFile(String url) {
      // Identificar las imágenes por hash
      String filename = String.valueOf(url.hashCode());
      File f = new File(cacheDir, filename);
      return f;
   }
   public void clear() {
      // Listar todos los archivos de la cache
      File[] files = cacheDir.listFiles();
      if (files == null) return;
      // Borrar todos los archivos del directorio
      for (File f : files) f.delete();
   }
}

Ya casi terminando, nos queda la clase que se encargará de bajar las imágenes y guardarlas en la cache

public class BajarImagenes {
// Inicializar MemoriaCache
MemoriaCache memoryCache = new MemoriaCache();
ArchivoCache archivoCache;
// Creamos una colección (map) para guardar los pares de datos
// imagen y su respectiva URL
private Map<ImageView, String> imageViews = Collections .synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
// handler para mostrar la imagen en la UI
Handler handler = new Handler();
public BajarImagenes(Context context) {
archivoCache = new ArchivoCache(context);
// Creamos un grupo de subprocesos que utiliza un numero
// fijo de hilos que bajarán las diferentes imágenes encoladas
executorService = Executors.newFixedThreadPool(5);
}
// Esta es la imagen por defecto para mostrar
// mientras se baja la que corresponde, o en caso
// de que ocurra algun error en la bajada
final int stub_id = R.drawable.nodisponible;
public void DisplayImage(String url, ImageView imageView) {
// Guardamos la imagen y su URL en la collección
imageViews.put(imageView, url);
// Verificamos si la imagen se encuentra en cache
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null) {
// Si la imagen la tenemos en nuestra SD, la mostramos
imageView.setImageBitmap(bitmap);
} else {
// encolamos para bajar la URL de la imagen
queuePhoto(url, imageView);
// Y antes de comenzar a bajar, mostramos la imagen por defecto
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, ImageView imageView) {
// Guardamos la imagen y su url en un objeto PhotoToLoad
PhotoToLoad p = new PhotoToLoad(url, imageView);
// Pasamos el objeto PhotoToLoad a la clase PhotosLoader
// y enviamos PhotosLoader al executor para realizar la tarea
// del bajado de fotos
executorService.submit(new PhotosLoader(p));
}
// Tarea a encolar
private class PhotoToLoad {
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i) {
url = u; imageView = i;
}
}
class PhotosLoader implements Runnable {
PhotoToLoad photoToLoad;
PhotosLoader(PhotoToLoad photoToLoad) {
this.photoToLoad = photoToLoad;
}
@Override
public void run() {
try {
// Verificamos si la imagen ya está bajado
if (imageViewReused(photoToLoad)) return;
// bajamos la imagen desde su URL
Bitmap bmp = getBitmap(photoToLoad.url);
// Guadamos la imagen en la cache
memoryCache.put(photoToLoad.url, bmp);
if (imageViewReused(photoToLoad)) return;
// Obtenemos el Bitmap a mostrar
BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
// Hacemos que el BitmapDisplayer se agregue a la cola de mensajes
// Este se ejecutará en un un subproceso separado asociado al handler
//que definimos inicialmente.
handler.post(bd);
} catch (Throwable th) {
th.printStackTrace();
}
}
}
private Bitmap getBitmap(String url) {
File f = archivoCache.getFile(url);
// Verificamos si estamos tratando de decodificar un archivo que
// exite en nuestra cache
Bitmap b = decodeFile(f);
if (b != null) return b;
// OK, no existe. Bajamos la imagen desde la web
try {
Bitmap bitmap = null;
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl .openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is = conn.getInputStream();
// Creamos un nuevo FileOuputStream para guardar nuestro archivo.
OutputStream os = new FileOutputStream(f);
// Cada pixel bajado de la web, lo pasamos a nuestro
//nuevo archivo
Utils.CopyStream(is, os);
os.close();
conn.disconnect();
// Tenemos el archivo. Vamos a redimensionarlo para reducir
// el consumo de memoria. Esto previene que si la url apunta
// a una foto sacada con una cámara de 20 megapixels,
// guardemos un archivo mountroso
bitmap = decodeFile(f);
return bitmap;
} catch (Throwable ex) {
ex.printStackTrace();
if (ex instanceof OutOfMemoryError) memoryCache.clear();
return null;
}
}
// Decodifica la imagen y la reduce a un tamaño adecuado
private Bitmap decodeFile(File f) {
try {
// Decodifica la imagen
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream stream1 = new FileInputStream(f);
BitmapFactory.decodeStream(stream1, null, o);
stream1.close();
// Seteamos el ancho (y la altura) que tendrá la imagen.
// Debe ser divisible por 2
final int REQUIRED_SIZE = 100;
int width_tmp = o.outWidth, height_tmp = o.outHeight;
int scale = 1; while (true) {
if (width_tmp / 2 < REQUIRED_SIZE || height_tmp / 2 < REQUIRED_SIZE) break;
width_tmp /= 2;
height_tmp /= 2;
scale *= 2;
}
// Decodificar con los valores actuales luego de redimensionar
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
FileInputStream stream2 = new FileInputStream(f);
Bitmap bitmap = BitmapFactory.decodeStream(stream2, null, o2);
stream2.close();
return bitmap;
}
catch (FileNotFoundException e) {
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
boolean imageViewReused(PhotoToLoad photoToLoad) {
String tag = imageViews.get(photoToLoad.imageView);
// Verificar si la url ya existe en nuestra collección
if (tag == null || !tag.equals(photoToLoad.url)) return true;
return false;
}
// Usada para mostrar la imagen en UI
class BitmapDisplayer implements Runnable {
Bitmap bitmap;
PhotoToLoad photoToLoad;
public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
bitmap = b; photoToLoad = p;
}
public void run() {
if (imageViewReused(photoToLoad)) return;
// Mostrar el bitmap en UI
if (bitmap != null) photoToLoad.imageView.setImageBitmap(bitmap);
else photoToLoad.imageView.setImageResource(stub_id);
}
}
public void clearCache() {
// Borrar la cache
memoryCache.clear();
archivoCache.clear();
}
}

Por último sólo nos queda modificar el adaptador de nuestra ListView para integrar todo lo que creamos hasta ahora.

...
...
public BajarImagenes bajarImagenes;
public ComidaAdaptador(Context context, List<Comida> items) {
   super(context, R.layout.lv_layout, items);
   this.items = items;
   bajarImagenes = new BajarImagenes(this.getContext());
}
...
...
   // urlimg ----- ESTO LO RESOLVEMOS LUEGO
   ImageView imagen = (ImageView) v.findViewById(R.id.imgComida); System.out.println("***** URL IMAGEN:" + com.getUrlimg());    bajarImagenes.DisplayImage(com.getUrlimg(), imagen);
...
...

En este punto podemos ejecutar nuestra app.

En esta captura, no subí las imágenes aún, por lo cual fallarán todas las URL y mostrará la imagen por defecto.

Esta otra captura, subí algunas de las imágenes.

Y finalmente, este es el contenido de la cache.

Esta cuarta y última parte, la implementación de la cache, está basado en este artículo: http://androidexample.com/Download_Images_From_Web_And_Lazy_Load_In_ListView_-_Android_Example/index.php?view=article_discription&aid=112&aaid=134


FacebookMySpaceTwitterDiggDeliciousStumbleuponGoogle BookmarksRedditNewsvineTechnoratiLinkedinRSS FeedPinterest
Pin It