«

»

mar 21 2012

Selección individual/múltiple de elementos en un ListView

Aprovechando tanto una pregunta que me hizo @JMPergar en G+ como unos commits que hizo @sloy5101 en Sevibus, me decidí por fin a escribir mi primer tutorial (espero que de una larga serie) para Androcode :)

Infinidad de veces me he encontrado con aplicaciones que requerían la visualización en listas de elementos seleccionables por el usuario. Ya sea para marcar una o varias opciones en una lista, para efectuar una serie de operaciones sobre uno o varios elementos de la lista, para saber qué elemento de la lista está siendo mostrado en otra vista (modelo típico de tablets), etc.

Lo malo es que tabién infinidad de veces me he encontrado con código como el siguiente para implementar dicha funcionalidad:

@Override
View public getView(int position, View convertView, ViewGroup parent) {
	if(position == miPosicionSeleccionada){
		//Cambiar el fondo, activar el checkbox de turno, poner un icono a la derecha, etc
	}
}

 

¿Qué tiene esto de malo?

Bien, en principio nada… pero veamos…

 

Ese código suele ir en la implementación de nuestros Adapters, por lo que para que funcione, necesitaríamos 2 cosas:

  1. Que cada vez que se seleccione un elemento de la lista, le digamos al adapter que actualice TODAS las vistas. (típicamente con Adapter.notifyDataSetChanged())
  2. Que el adapter sea una clase propia que nos permita sobreescribir el método getView()
  3. Que tengamos que estar almacenando en variables los elementos donde el usuario va pinchando para modificar el adapter, refrescarlo y posteriormente saber a qué elementos aplicar las operaciones que queremos (eliminar, marcar como favoritos,etc).

 

¿Entonces es malo o no?

No, no es malo.
Esta forma de trabajar es la correcta cuando hablamos de modificar permamentemente los datos de dentro de nuestro Adapter. Por ejemplo, en una aplicación donde presentamos al usuario una lista de elementos y cada elemento puede estar marcado como favorito o no usando la típica estrellita, el modo de realizarlo es el comentado anteriormente. ¿Por qué? Porque el hecho de cambiar el estado de “favorito” o “no favorito”, implica que estamos modificando los datos que muestra ese adapter.
Pero en el caso de realizar “selecciones” es algo temporal, algo que no persiste en el tiempo de vida de nuestra aplicación. Y por eso no se debería usar este método.
 

¿Entonces cómo se hace?

Veamos, desde android 1, los ListViews (y desde android 3.x los GridViews también, aunque la documentación dice incorrectamente que lo soportan desde la api 1) ya mantienen una implementación para el patrón de uso que necesitamos (seleccionar uno/varios elementos) y su uso y disfrute es bien sencillo y sobre todo fácil de mantener.

 

Paso 1, decidir si nuestra lista permite selección de uno o de varios elementos y automáticamente hace que al pinchar en la lista se seleccionen los elementos al pulsar sobre ellos:

miListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
miListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

 

Paso 2, cuando queremos hacer cosas con los elementos seleccionados de nuestra lista:

SparseBooleanArray seleccionados = myLv.getCheckedItemPositions();

 

Paso 3 (opcional):

  • cuando queramos seleccionar manualmente un elemento de la lista:
miListView.setItemChecked(posicion);
  • cuando queramos saber si un elemento concreto está seleccionado:
miListView.isItemChecked(posicion);

Ya está, fácil, eh?

 
Veamos un ejemplo completo en el que pulsando un elemento de la lista, lo selecciona y pulsando en un botón se simula el borrado de los elementos seleccionados:

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<Button
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Borrar los seleccionados"
    android:onClick="deleteSelected"
    />
<ListView android:id="@+id/lista"
          android:layout_width="fill_parent"
          android:layout_height="0dp"
          android:layout_weight="1"/>
</LinearLayout>

res/layout/row1.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/titulo"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:textAppearance="@android:style/TextAppearance.Large"
          android:textColor="#FFDEDEDE"/>

Activity de ejemplo

public class EjemploActivity1 extends Activity {
    private ListView miLista;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        miLista = (ListView)findViewById(R.id.lista);
	//pongo mi lista en modo de selección múltiple
	//con esto, al hacer click en los elementos se seleccionarán automáticamente
        miLista.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);

        //Hago un ArrayAdapter sencillo con muchas letras A :P 
        String[] nombres=new String[]{"A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A"};
        ArrayAdapter<String> data=new ArrayAdapter<String>(this,R.layout.row1,R.id.titulo, nombres);
        //Le asigno ese adapter a la lista
        miLista.setAdapter(data);
    }

    /**
     * Este método se llama al pulsar en el botón de borrar
     * como se definió en el layout res/layout/main.xml
     */
    public void deleteSelected(View view) {
        //Obtengo los elementos seleccionados de mi lista
        SparseBooleanArray seleccionados = miLista.getCheckedItemPositions();

        if(seleccionados==null || seleccionados.size()==0){
            //Si no había elementos seleccionados...
            Toast.makeText(this,"No hay elementos seleccionados",Toast.LENGTH_SHORT).show();
        }else{
            //si los había, miro sus valores

            //Esto es para ir creando un mensaje largo que mostraré al final
            StringBuilder resultado=new StringBuilder();
            resultado.append("Se eliminarán los siguientes elementos:\n");

            //Recorro my "array" de elementos seleccionados
            final int size=seleccionados.size();
            for (int i=0; i<size; i++) {
                //Si valueAt(i) es true, es que estaba seleccionado
                if (seleccionados.valueAt(i)) {
                    //en keyAt(i) obtengo su posición
                    resultado.append("El elemento "+seleccionados.keyAt(i)+" estaba seleccionado\n");
                }
            }
            Toast.makeText(this,resultado.toString(),Toast.LENGTH_LONG).show();
        }
    }
}

Si probamos ahora la aplicación, veremos lo siguiente:

  1. Pulsando sobre un elemento, nos aparecerá un mensaje diciendo si está “true” o “false” (seleccionado o no)
  2. Pulsando el botón de “Borrar” nos mostrará una lista con los elementos que teníamos seleccionados.

Pero algo falla….. Cuando selecciono un elemento NO me lo “pinta” de otro color o algo que destaque!!!
Bueno, es que eso son cosas diferentes, separación de la funcionalidad con respecto al diseño :)

 

Vale, ¿y cómo se hace?

Vamos a modificar el primer ejemplo con algo sencillo:
Primero necesitamos un par de imágenes o drawables para que los elementos se pinten de una forma u otra dependiendo de si están seleccionados o no. En este caso voy a crear 2 drawables que son siemplemente un rectángulo de color. Se podría cambiar este paso por 2 PNGs o lo que queráis.

res/drawable/fondo_seleccionado.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle" >
    <solid android:color="#FFFF0000"/>
</shape>

res/drawable/fondo_no_seleccionado.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle" >
    <solid android:color="#00000000"/>
</shape>

Ahora necesito un tercer Drawable que sirva para combinar los 2 anteriores en base a su estado:

res/drawable/selector_filas.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:drawable="@drawable/fondo_seleccionado" />
    <item android:drawable="@drawable/fondo_no_seleccionado" />
</selector>

Y por ultimo, necesito asignar este último drawable a nuestras filas de la lista:

res/layout/row1.xml

<

<?xml version=”1.0″ encoding=”utf-8″?>
<TextView xmlns:android=”http://schemas.android.com/apk/res/android”
          android:id=”@+id/titulo”
          android:layout_width=”match_parent”
          android:layout_height=”wrap_content”
          android:textAppearance=”@android:style/TextAppearance.Large”
          android:textColor=”#FFDEDEDE”
          android:background=”@drawable/selector_filas”/>

Si ejecutáseis de nuevo la aplicación ahora, veríais que sigue sin funcionar (lo de cambiar los colores de los elementos seleccionados) y esto es debido a que los TextView normales NO guardan información del estado “checked” (vamos, que un TextView no sabe si está “checked” o “no checked”).
Pero si cambiáis el archivo res/layout/row1.xml poniendo un CheckedTextView en lugar del TextView….

res/layout/row1.xml

<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/titulo"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:textAppearance="@android:style/TextAppearance.Large"
          android:textColor="#FFDEDEDE"
          android:background="@drawable/selector_filas"/>

Tachán! La magia funciona!

 

¿Y por qué? ¿Y qué es eso del CheckedTextView? ¿Y si mis filas de la lista son complejas con imágenes, tectos, etc?

En Android, teóricamente, todo widget que necesite contemplar los estados “checked”, debe implementar la interfaz Checkable. Esta interfaz provee a nuestras clases de métodos para gestionar dichos estados. Del mismo modo que CheckedTextView es (casi) un TextView que implementa la interfaz Checkable, nosotros podemos crear nuestros propios widgets que hagan lo mismo.

Vamos a mejorar nuestro ejemplo cambiando el layout que usamos en los elementos de la lista por algo más complejo:

res/layout/row2.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@drawable/selector_filas">
    <TextView
              android:id="@+id/titulo"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:textAppearance="@android:style/TextAppearance.Large"
              android:textColor="#FFDEDEDE"/>
    <TextView
            android:id="@+id/descripcion"
            android:text="descripciones y tal..."
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAppearance="@android:style/TextAppearance.Small"
            android:textColor="#FFC3C3C3"/>

</LinearLayout>

Y ahora modificamos el código donde creábamos nuestro Adapter para usar este nuevo layout en lugar del anterior:

ArrayAdapter<String> data=new ArrayAdapter<String>(this,R.layout.row2,R.id.titulo, nombres);

Si probamos ahora la aplicación, nuestras filas estarán formadas por varios elementos (2 textviews pero podríais poner lo que queráis) pero nuestro invento de antes para pintar de rojo las filas seleccionadas ha dejado de funcionar como ocurría antes cuando usábamos un TextView en las filas…
Pero claro, antes cambiamos el TextView por un CheckedTextView y tan contentos… ¿Habrá un CheckedLinearLayout para hacer el mismo truco?
Pues sí, claro que lo hay, el que vamos a crear nosotros :) (claro que si buscáis en Google lo mismo encontráis alguno más)

Vamos a crear una clase nueva que extienda de LinearLayout con el siguiente código (leed los comentarios del código!! :)

CheckedLinearLayout.java

/**
 * LinearLayout que implementa la interfaz Checkable para
 * gestionar estados de selección/deselección
 */
public class CheckedLinearLayout extends LinearLayout implements Checkable{

    /**
     * Esta variable es la que nos sirve para almacenar el estado de este widget
     */
    private boolean mChecked=false;

    /**
     * Este array se usa para que los drawables que se usen
     * reaccionen al cambio de estado especificado
     * En nuestro caso al "state_checked"
     * que es el que utilizamos en nuestro selector
     */
    private final int[] CHECKED_STATE_SET = {
            android.R.attr.state_checked
    };

    public CheckedLinearLayout(Context context) {
        super(context);
    }

    public CheckedLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Este método es el que cambia el estado de nuestro widget
     * @param checked true para activarlo y false para desactivarlo
     */
    @Override
    public void setChecked(boolean checked) {
        mChecked = checked;
        //Cuando cambiamos el estado, debemos informar a los drawables
        //que este widget tenga vinculados
        refreshDrawableState();
        invalidate();
    }

    /**
     * Este método devuelve el estado de nuestro widget :) 
     * @return true o false, no?
     */
    @Override
    public boolean isChecked() {
        return mChecked;
    }

    /**
     * Este método cambia el estado de nuestro widget
     * Si estaba activo se desactiva y viceversa
     */
    @Override
    public void toggle() {
        setChecked(!mChecked);
    }

    /**
     * Este método es un poco más complejo
     * Se encarga de combinar los diferentes "estados" de un widget
     * para informar a los drawables.
     *
     * Si nuestro widget está "checked" le añadimos el estado CHECKED_STATE_SET
     * que definimos al principio
     *
     * @return el array de estados de nuestro widget
     */
    @Override
    public int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
        }
        return drawableState;
    }
}

Ahora modificamos el layout de nuestros elementos de la lista para usar nuestra clase CheckedLinearLayout en lugar del LinearLayout normal:

res/layout/row2.xml

<?xml version="1.0" encoding="utf-8"?>
<el.package.name.completo.de.CheckedLinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@drawable/selector_filas">
    <TextView
              android:id="@+id/titulo"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:textAppearance="@android:style/TextAppearance.Large"
              android:textColor="#FFDEDEDE"/>
    <TextView
            android:id="@+id/descripcion"
            android:text="descripciones y tal..."
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAppearance="@android:style/TextAppearance.Small"
            android:textColor="#FFC3C3C3"/>

</el.package.name.completo.de.CheckedLinearLayout>

 

NOTA: Recordad cambiar “el.package.name.completo.de.CheckedLinearLayout” por el packagename de vuestra aplicación :)

¡Listo! Ya tenemos una lista con filas “complejas” que permite selección múltiple y que cambia los colores de las filas cuando están seleccionadas, etc.
 

Y ahora diréis… pues esto es más complicado que cambiar por código el fondo desde el getView() de un adapter…
Pues no, realmente no lo es. La única parte “compleja” es tener que crear nuestros widgets para que implementen la clase Checkable.
Pero lo bueno es que, visto uno, vistos todos. Podríais tener vuestro propio “paquete” de clases con todos los layouts típicos que uséis en vuestras aplicaciones (LinearLayout, RelativeLayout, FrameLayout, etc) implementando la clase Checkable y así poder reutilizarlos en todos los proyectos.

 
¿Qué ventajas obtendríais?
Que a partir de ahora, los cambios de diseño en los layouts que usáis como filas en los adapters sólo afectarán a esos XML y no tendréis que andar tocando código :)
Ah, y que queda más “profesional” :P
 

Acerca del autor

AnderWeb

Coding enthusiast, tech lover, lazy guy, freaky nerd, porque espain is diferent

  • http://about.me/sloy Sloy | Editor

    Genial! Cuando arregle los tropecientos bugs que han aparecido tras la actualización haré esto bien :D

  • danigonlinea

    Simplemente estupendo. Felicidades por tu primer tutorial aquí en androcode y te animo para que no sea el último!! ;)

  • http://alexrs95.wordpress.com Alexrs95

    ¿A partir de qué versión se puede implementar esta funcionalidad?
    ¡Muy buen tutorial! :)

  • AngeL

    Muy bueno!!!
    Muchas gracias!!!!

    Aunq andaba buscando un listview con checkbox, 2 textview y un imageview, a ver si puedo adaptar esto!!

    Sigue asi!!

    PD: Si puedes hacer un tuto de lo q estoy buscando te lo agradeceré infinito, jeje

  • daniel

    hola tengo un problema VISUAL en 2.3 o inferior, cuando selecciono un renglon me selecciona otro, la logica si esta bien, el problema es visual

  • Ramon

    Estoy siguiendo este ejemplo y tengo el siguiente problema , me da el error por una excepcion

    java.lang.NumberFormatException: Color value ‘@drawable/selector_filas’ must start with #

    es los xml fondo_seleccionado y fondo_no_seleccionado

    –> PARECE SER AKI PERO NO SE PORQUE ES ??

    Gracias

  • Pingback: Selección individual/múltiple de elementos en un ListView | AndroidDSI()

  • damian ippolito

    Hola, primero, muy bueno el post.
    Yo estoy intentando hacer algo muy parecido para un proyecto laboral que estoy desarrollando.
    La unica diferencia es que mis filas de la lista estan formadas por un RemoteImageView, un TextView y un ImageView
    Mi unico problema es que tal y como lo has hecho tu, no me carga las imagenes en los RemoteImageView ya que esa misma lista, tambien la creo en otra activity sin posibilidad de multiseleccion a traves de un adaptador sobreescrito que he hecho yo(donde le digo al RemoteImageView, que imagen debe mostrar)

    Si pudieras ayodarme, te lo agradeceria muchisimo porque esto es para mi trabajo, no por hobby y tengo que entregarlo el 14 de octubre

  • https://www.facebook.com/marcosgarciagimenez Marcos Garcia Gimenez

    Increiblemente bueno. Muchas gracias!!!

  • Juan

    Me ha gustado mucho como has planteado este tutorial.
    Lo unico bueno que he visto en lo que ha seleccionar filas de un ListView se refiere.
    Buen trabajo y gracias por tu aportacion.
    Un saludo.

  • joan

    No hay alguna forma, de evitar que se pinte cuando se clikea? solo quiero q se pinte cuando se deja apretado. yo lo parche pero no me gusta.
    ListView lista;
    public void onItemClick(AdapterView adapterView, View view, int i, long l) {
    vista.setItemChecked(i,false);
    //resto del codigo
    }
    saludos