Programación Android, ListView & Adapter personalizado II

Share if you like...Share on Facebook0Tweet about this on TwitterShare on Google+0Share on LinkedIn0

En la anterior entrada creamos un Adapter personalizado para mostrar una vista creada por nosotros en cada ítem del ListView, pero ya comentamos que tenía un problema de optimización.

Este problema consistía en que siempre estábamos creando una nueva vista por cada ítem que contenía la colección que queríamos mostrar en el ListView, por lo que si utilizamos una colección muy grande, creamos demasiadas vistas, lo que consumiría mucha memoria (algo sobre lo que prestar mucha atención en un smartphone).

En realidad esto no es del todo cierto, Android lo que hace es almacenar sólo 13 de estas vistas del ListView en la memoria. Sin embargo sí que estamos haciendo demasiadas llamadas a crear vistas en el anterior Adapter, problema que se arregla reutilizando las vistas, teniendo en cuenta que ya previamente las habíamos creado.

Todos los cambios que tendremos que hacer en nuestro Adapter los tendremos que realizar dentro del método getView(). Lo primero será controlar si el parámetro que nos llega del tipo View es null o no. Si es null significa que esta vista no la habíamos creado aún, por lo que tendremos que crearla. Sin embargo si no viene a null sí que estará ya creada, y que tendremos que hacer será reutilizarla.

Patrón Holder

En primer lugar vamos a crear una nueva clase, que será un contenedor de las referencias a los controles que hemos incluido en la vista del ítem, es decir, simplemente crearemos declaraciones públicas de los controles que añadimos a la vista:

import android.widget.ImageView;
import android.widget.TextView;

public class AnimalesHolder {
	public ImageView imgAnimal;
	public TextView tvField, tvContent;
}

Optimizando el Adapter

Declaramos un objeto de este tipo en el método getView() de nuestro Adapter, igualamos el objeto de la clase View que instanciamos con el que nos viene por parámetro, y controlamos si viene a null:

AnimalesHolder holder;
View item = convertView;

if(item == null) {

}

Dentro del cuerpo del if inicializaremos la vista, ya que sería null, e inicializaremos el holder. Después le daremos los valores al holder del ImageView y los TextView que tiene que almacenar, para posteriormente poder manipular esos controles:


item = LayoutInflater.from(context).inflate(R.layout.listview_item,
		null);

holder = new AnimalesHolder();
holder.imgAnimal = (ImageView) item.findViewById(R.id.imgAnimal);
holder.tvContent = (TextView) item.findViewById(R.id.tvContent);
holder.tvField = (TextView) item.findViewById(R.id.tvField);

Sin embargo esta inicialización del holder sólo sucede dentro del bloque if, por lo que en el caso de que la vista no sea null el holder nunca se habrá inicializado, ni contendrá las referencias a los controles. Para solucionar esto, en el objeto View tenemos un método que nos permite guardar cualquier objeto, para poder recuperarlo después. Este método es setTag(Object obj), y para poder recuperarlo posteriormente lo haríamos a través de getTag().

Por lo tanto, en la última línea del bloque if guardaremos el holder dentro de la vista:

item.setTag(holder);

Así, después del if la vista siempre contendrá en su interior el holder (si venía a null lo introdujimos en el if, y si no venía a null es que ya se había hecho previamente), por lo tanto sólo tendremos que recogerlo. Sin embargo getTag() lo que devuelve es un objeto de tipo Object, que es el tipo del que heredan implícitamente todos los objetos en Java, por lo que para poder asignárselo de nuevo al holder tendremos que hacer un casting a AnimalesHolder:

holder = (AnimalesHolder) item.getTag();

Y ahora ya podremos manipular los TextView y el ImageView para asignarle los valores que queramos, a través de nuestro objeto holder:

holder.imgAnimal.setImageResource(datos.get(position).getDrawableImageID());
holder.tvContent.setText(datos.get(position).getNombre());
holder.tvField.setText(String.valueOf(position));

El método completo getview() quedaría finalmente así:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	View item = convertView;
	AnimalesHolder holder;

	if (item == null) {
		item = LayoutInflater.from(context).inflate(R.layout.listview_item,
				null);

		holder = new AnimalesHolder();
		holder.imgAnimal = (ImageView) item.findViewById(R.id.imgAnimal);
		holder.tvContent = (TextView) item.findViewById(R.id.tvContent);
		holder.tvField = (TextView) item.findViewById(R.id.tvField);

		item.setTag(holder);
	}

	holder = (AnimalesHolder) item.getTag();

	// A partir del holder, asignamos los valores que queramos a los
	// controles.
	// Le asignamos una foto al ImegeView.
	holder.imgAnimal.setImageResource(datos.get(position)
			.getDrawableImageID());

	// Asignamos los textos a los TextView.
	holder.tvContent.setText(datos.get(position).getNombre());
	holder.tvField.setText(String.valueOf(position));

	// Devolvemos la vista para que se muestre en el ListView.
	return item;
}

Y con esto ya habremos creado un Adapter personalizado a nuestros intereses, que además quedará totalmente optimizado para que utilice los menores recursos posibles del dispositivo.

Descargas

Puedes ver el resultado del ejemplo en tu móvil descargando nuestra aplicación desde Google Play.

Puedes acceder a nuestro repositorio para bajar el proyecto aquí.

Share if you like...Share on Facebook0Tweet about this on TwitterShare on Google+0Share on LinkedIn0
The following two tabs change content below.
Reborn as IT Developer. Desarrollador Android y fundador de Proyecto Simio. "En realidad, yo no puedo enseñar nada a nadie, sólo puedo hacerles pensar." - Sócrates.

16 thoughts on “Programación Android, ListView & Adapter personalizado II

  1. Igor primero que todo muchas gracias por tus conocimiento no sabes el apoyo que uno encuentra.
    la cuestión es la siguiente.
    ya cree el lisview que contiene la imagen y tres textos para la información, deseo Recuperar la información del item seleccionado, como la informacion se incresó en una clase y dicha clase dirigió la información al layout
    hay forma de obtener la informacion del item seleccionado ??

    item:
    -> foto
    -> nombre
    -> ident

    muchas gracais

    1. Buenas Alejandro, pues entiendo que en cualquier caso tendrás una clase modelo para guardar esa información y que el array que utilices para llenar el listview será de ese tipo (arraylist). En ese caso con tener el adapter del listview solo tendrás que hacer adapter.getItem(posición), donde la posición es la del elemento del listview que se haya pulsado y que te viene en el propio evento del listview.

      Muchas gracias a ti por el comentario :)

  2. Hasta ahora lo que he logrado es activar los checkbox de cada item y capturar la informacion de los editText, pero no tengo idea de como almacenar la información de todos los items seleccionados para utilizarlos en mi activity principal.

    1. Para hacer esto tendrías que añadir al modelo de datos que utilizas en el adaptador campos para el checkbox y el edittext y después implementarles listeners a los controles en el adapter del listview para que recojan los cambios (si se activa o desactiva el checkbox y si cambia el texto del edittext) y que automáticamente hagan que se actualicen los datos en la colección también, por ejemplo:

      cb.setOnCheckedChangeListener(new OnCheckedChangeListener( ...
      
      datos.get(position).setChecked(cb.isChecked());
      ...);
      

      Para controlar el cambio de texto en el edittext tendrías que usar un TextWatcher.

      Un saludo.

  3. hola Necesito crear un ListView con un checkbox y editText, además necesito saber cuales items (checkbox seleccionado) estan seleccionado y capturar la info de los EditText, para posteriormente usarla en la actividad principal. Gracias

  4. Buenas Igor,

    Me vino de perlas tu ejemplo!! he estado unos días fueras, pero lo acabo de revisar y ya consigo que el botón responda!! :) muchisimas gracias!! me daba error, porque no tenía bien definido el Adapter, extendía de BaseAdapter en lugar de ArrayAdapter, y además tenía un poco de lío con la vista en la que me econtraba y me daba un null pointer exception al pichar sorbre el botón. Ahora me queda que al pinchar en el textView me funcione también…que eso, me temo que me va a dar algo más de guerra…. Muchisimas gracias por toda la ayuda Igor!!
    Saludos

  5. Hola a todos,
    Quería saber si es posible tener un ListView que mostrara un ImageView, un TextView y un botón (creo que sería lo que necesito) en el que se pudiera diferenciar entre pulsar sobre el TextView o sobre el botón. También, si podría hacer de alguna manera que el botón solo apareciera e unas condiciones determinadas al igual que solo se pudiera pulsar sobre el TextView si otra determinada condición se cumple…. no se me ocurre como hacer todo eso. No se si hay que hacerlo desde el adapter, desde el holder o desde la activty. Muchas gracias por todo!! Saludos

    1. Sí es posible, las vistas las puedes diseñar como quieras, sólo tienes que crear tu propio layout para el item del ListView.

      En cuanto a las pulsaciones sobre determinados controles que comentas, tendrías que codificarlo en el método getView del adapter, donde crearías el listener para cada control que lo necesite, y habilitarías o no el botón en función de las condiciones que necesitaras.

      La parte en que tendrías que configurarlo es justo después inicializar todos los controles que contiene el holder.

      Un saludo Elena.

      1. Hola de nuevo,

        Antes de nada, muchas gracias por contestar siempre así de rápido y claro! :)
        He conseguido personalizar mi layout y que el botón solo se muestre en aquellos casos en los que es necesario (aunque no funciona del todo vien, pues en las siguientes llamadas al adaptador…no sale, pero ese es otro tema que aún estoy pendiente de revisar), pero la parte de crear el listener para cada control….esa si que no soy capaz y es que no lo acabo de entender. ¿Cómo podría hacerlo? ¿Con un switch? Muchas gracias!
        Elena

        1. Hola Elena. Para suscribir el evento al botón del item no tienes más que, después de recoger el holder e inicializar los controles (TextView, Button, etc.), al botón le haces un button.setOnClickListener(new OnClickListener() {… });

          Así le suscribes a cada botón su propio listener, para que haga exactamente el trabajo que tenga que hacer.

          Por ejemplo, si el botón es para editar el item, como el listener lo estás implementando dentro del método getView, tendrás acceso a la variable “position”, por lo que con hacer un “getItem(position)” ya tienes el elemento que se encuentra en esa posición, para poder pasárselo a otra activity y poder editarlo.

          Espero que te haya quedado más claro :)

          1. Hola Igor!
            Estoy trabajando fuertemente en ello….jajaja, de momento aún no lo he conseguido, pero no desisto. El problema es que en el momento en el que le asigno el método setOnClickListener al botón, lo que pasa después es que no puedo hacer click en ninguna parte de la fila, pero en las filas que no llevan el boón (por la condición que impongo) si puedo hacerlo…. raro no? jajaja. Voy a intentar esta tarde ver donde puede estar el error, porque debería poder hacer click en cualquiera de los dos controles, el botón o el textview…
            Muchas gracias por todo, la explicación fue genial y me vino muy bien!! :)
            Elena

          2. Me alegro de que te haya servido la explicación :)

            Pues sí es raro que no te funcione, te copio el ejemplo, en este caso un ImageView, de un elemento del item al que le asigné un evento onClick, y el resto del item funcionaba perfectamente con su onItemClick. Esto va en el Adapter como te dije:

            holder.ivInfo.setOnClickListener(new OnClickListener() {
            
            	@Override
            	public void onClick(View v) {
            		Intent i = new Intent(context, DetalleArticulo.class);
            		i.putExtra("articulo", articulos.get(position));
            
            		context.startActivity(i);
            	}
            });
            

            Espero que esto te sirva de guía y puedas solucionar tu error :)

            Un saludo!

  6. Hola, espero alguien pueda ayudarme, quiero sincronizar 3 spinner, es decir, cuando se seleccione el indice de X spinner automaticamente cambie en los otros dos y viceversa.

    Gracias, un saludo, son bienvenidas todas las posibilidades

    1. Buenas Paloma,

      Puedes echar un ojo a la entrada Programación Android, Cómo usar un spinner II

      En esta entrada explico el uso del evento OnItemSelected del Spinner. En el código del método onItemSelected de cada Spinner tendrías que hacer la selección del item que necesitaras en los otros dos Spinner. Ejemplo:

      spinner1.setOnItemSelectedListener(new OnItemSelectedListener() {

      @Override
      public void onItemSelected(AdapterView<?> adapter, View vies,
      int position, long id) {
      spinner2.setSelection(position);
      spinner3.setSelection(position);
      }

      @Override
      public void onNothingSelected(AdapterView<?> adapter) {

      }
      });

      Espero que te sirva, un saludo.

Deja un comentario