Solucionando el problema de los ListView con Checkbox en Android

Publicado por Alejandro Escario en

Android es un gran sistema operativo para móviles que le supone una competencia directa al iOS, y por lo tanto meterse como programador en su mundo puede ser algo muy atractivo. Pero como programadores más o menos experimentados sabemos que no todo es un paseo, tal y como algunos pretenden plantearlo; siempre surgen problemas que habremos de solucionar. Hoy, vamos a ver como evitar que, al tener un ListView en el que cada fila contenga un CheckBox, al hacer scroll, los elementos de la parte no visible se marquen al marcar uno de la parte visible de manera automática.

¿Por qué?

Esto sucede porque Android reutiliza las mismas vistas una y otra vez a la hora de pintar los elementos en pantalla, de manera que aunque el CheckBox que ha de mostrar no está seleccionado, se mostrará como tal dado que uno visible anteriormente ya lo estaba. De este modo se ahorra una gran cantidad de ciclos de CPU a la hora de generar la imagen que se va a mostrar.

Cómo podemos ver, el comportamiento por defecto del sistema operativo no siempre nos viene bien ya que, en casos como este, puede dar lugar a confusiones en el usuario final de la aplicación.

Solución

La manera de solucionar este pequeño inconveniente, aunque no de la forma más óptima, es tan simple como establecer a mano el valor del CheckBox que ha de mostrarse en cada momento, para ello tendremos que apoyarnos en una serie de clases y ficheros XML.

Antes de nada, vamos a analizar los ficheros que componen nuestro Layout, es decir, el que establece la distribución de la pantalla principal y el que establece el aspecto de cada una de las filas.

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">
<ListView
android:id="@+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
</LinearLayout>

row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="fill_parent">
<CheckBox
android:id="@+id/chkItem"
android:visibility="visible"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

Tal y como se puede observar, hasta este momento no hemos hecho más que definir dos layouts, uno que contiene un ListView y otro que contiene el CheckBox que ha de aparecer en cada fila.

Prosigamos programando nuestra clase Java para manejar estos Layouts:

package org.dipler.chkList;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.CompoundButton.OnCheckedChangeListener;

public class Main extends Activity {
private ChkListAdapter adapter;
private ListView list;
private CheckBox chkAll;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
list = (ListView)findViewById(R.id.list);
fillList();
}

private void fillList() {
int num = 50;
adapter = new ChkListAdapter(num);
for(int i = 0; i < num; i++){
adapter.addItem(new Item(String.valueOf(i), "item number " + i));
}
list.setAdapter(adapter);
}

private class ChkListAdapter extends BaseAdapter {
private ArrayList<Item> items = new ArrayList<Item>();
private LayoutInflater inflater;
private boolean[] itemSelection;
public ChkListAdapter(int size) {
inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.itemSelection = new boolean[size];
}

public void addItem(final Item item) {
items.add(item);
notifyDataSetChanged();
}

@Override
public int getCount() {
return items.size();
}

@Override
public String getItem(int position) {
return items.get(position).toString();
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
convertView = inflater.inflate(R.layout.row, null);
final ViewHolder holder = new ViewHolder();
holder.chkItem = (CheckBox)convertView.findViewById(R.id.chkItem);
holder.chkItem.setOnCheckedChangeListener(new OnCheckedChangeListener(){
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
itemSelection[position] = holder.chkItem.isChecked();
}
});

holder.chkItem.setChecked(itemSelection[position]);
convertView.setTag(holder);
holder.chkItem.setText(getItem(position));
return convertView;
}

public int getItemsLength() {
if(itemSelection == null) return 0;
return itemSelection.length;
}

public void set(int i, boolean b) {
itemSelection[i] = b;
}

}

public static class ViewHolder {
public CheckBox chkItem;
}

}

Como podemos observar el la, bueno, las clases Java que hemos declarado tenemos:

  • La clase Activity principal que utiliza el Layout main.xml y que en el método onCreate() rellena el ListView con elementos, en nuestro caso de tipo Item (Item es una clase cualquiera con dos atributos, un id y un texto)
  • El adapter, que es la clase que se va a dedicar a repintar las filas tantas veces como sea necesario llamando al método getView() en cada una de esas ocasiones.
  • La clase ViewHolder que nos facilita el trabajo con la clase Row, esta clase no es en sí necesaria, pero vi la idea en amberfog y me gustó, de hecho de esta misma página obtuve una visión mucho más clara del funcionamiento de estos artificios llamados «Views»

El procedimiento para evitar que se marquen solos los CheckBox que cumplen el periodo, es sencillo, llevamos un array de booleanos en el que establecemos el estado de cada elemento de la lista confome cambia el valor del CheckBox, de manera que cada vez quedeseemos mostrar dicho elemento, el método getView(), en lugar de pintar como cree que debería pintar estos CheckBox, los muestra marcados o desmarcados en función del array que hemos creado y que vamos actualizando dinámicamente.

El método propuesto no es el más eficiente que nos podamos encontrar, pero funciona y nos puede ayudar a salir del paso.


26 comentarios

luis · septiembre 1, 2011 a las 12:58 am

Hola que tal quería consultarte de que formas vos decis de crear el array ya que el inflate es el que crea los elementos y los carga.

Saludos,
Luis

luis · septiembre 1, 2011 a las 1:01 am

Mi caso por ejemplo tegno un xml que defino un checkbox, 2 textview y 2 button, ya que preciso crear un renglon con estos elementos, pero me esta pasando que cuando selecciono un renglon me marca el check y controlo que no marque mas de 5 cuando lanzo un mensaje se me tara y no me deja desmarcar. La solución me parese que pasa por lo que comentas, pero como te comente antes no se donde crearias el array de elementos.

Saludos,
Luis

Alejandro Escario · septiembre 1, 2011 a las 3:07 pm

En mi caso y en el tuyo, lo mejor de todo es extender la clase de BaseAdapter, en mi caso le he llamado ChkListAdapter. Dentro de esta clase es donde creo el array con el contenido en cuestión.

En mi caso el contenido no es más que el de un booleano, pero en tu caso, lo mejor sería hacer un array de objetos que almacenen todos los datos relativos a tu fila de la lista.

Obviamente habría que crear los correspondientes métodos pra añadir y eliminar elementos de nuestra lista.

He resuelto tu duda?

Luis · septiembre 7, 2011 a las 1:07 pm

Si me ayudo, despues te cuento como me fue ya que voy a reporogramar la clase.

Saludos,
Luis

Carlos · noviembre 14, 2011 a las 5:33 am

Hola soy nuevo en android y tengo el mismo problema que expones, pero no se como adaptar tu solucion si jalo la info de una bd, aca te pego el codigo, para que me des una idea. Gracias de antemano

ingredientes.xml

listado_ingredientes.xml

clase ingredinetes.java

import android.app.ListActivity;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;

public class ingredientes extends ListActivity {
/** Called when the activity is first created. */

protected SQLiteDatabase db;
protected Cursor cursor;
protected ListAdapter adapter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.ingredientes);
db = (new DatabaseHelper(this)).getWritableDatabase();
cursor = db.rawQuery(«SELECT _id, nombre_ingrediente FROM ingredientes»,null);
adapter = new SimpleCursorAdapter(
this,
R.layout.listado_ingredientes,
cursor,
new String[] {«nombre_ingrediente»},
new int[] {R.id.txt_nombre_ingrediente});

setListAdapter(adapter);

}

Alejandro Escario · noviembre 14, 2011 a las 10:54 am

Es simple, lo que tienes que hacer con los datos que te devuelve la sentencia SQL es es añadirlos tanto a la lista(de la vista) como a la lista del Adapter, de esta manera se corresponden unos elementos con otros y el comportamiento será el adecuado.

me explico?

Carlos · noviembre 14, 2011 a las 4:33 pm

Hola ALejandro, la verdad que preferiria que me dieras un ejemplo para entenderlo mejor si no es mucha molestia soy realmente nuevo en el tema. Gracias

    Alejandro Escario · noviembre 15, 2011 a las 5:52 pm

    Es tan sencillo como, cuando inicialices el ListView, inicialices también el adaptador con el número de elementos que te devuelva la query de la base de datos SQLite; es decir, deberías poner el equivalente a lo que hay en el ejemplo bajo la función fillList():

    adapter = new ChkListAdapter(num);

    donde num es el número de elementos obtenidos de la sentencia.

    Espero que este mensaje te sirva de aclaración!

    un saludo!

Carlos · noviembre 16, 2011 a las 4:27 am

Alejandro, hasta ahí esta claro, en tu ejemplo la clase item ¿que es?

    Alejandro Escario · noviembre 16, 2011 a las 8:47 am

    […] en nuestro caso de tipo Item (Item es una clase cualquiera con dos atributos, un id y un texto) […]

    Es el objeto en el que insertamos cada uno de los elementos que queremos que aparezcan en el ListView es decir, es nuestro objeto que nos permite almacenar la información de cada una de las filas de la Lista y del cual obtenemos su contenido con elementos como:

    items.get(position).toString()

      Carlos · noviembre 16, 2011 a las 6:21 pm

      Yo trate de correr el ejemplo, pero me sale error, modifique el array de tipo item por uno de tipo string pero nada, si pudieras completar tu ejemplo para poder hacerlo correr. Gracias

        Alejandro Escario · noviembre 18, 2011 a las 1:36 pm

        Intentaré subirte un ejemplo, pero tengo que reescribirlo entero, ya que no lo tengo guardado; en cualquier caso, el objeto Item lo únicos parámetros que tiene son un int y un string con un método toString() que muestra lo que se ve en la lista, pero eso no te hace falta para llevar la cuenta de los checkBox, eso es independiente del contenido de la lista.

        Siento no poder darte el proyecto ya…

Carlos · noviembre 19, 2011 a las 5:59 pm

No te precoupes espero pacientemente por el contrario disculpame.

Carlos · noviembre 21, 2011 a las 12:39 am

Bueno mira hasta aquí llegue haciendo el simplecursorapdater personalizado, claro que no corrige el error aun, si puedieras darme la mano para lograrlo seria estupendo. Aca te pego el codigo.

public class ingredientes extends ListActivity {
/** Called when the activity is first created. */

protected SQLiteDatabase db;
protected Cursor cursor;
protected ListAdapter adapter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.ingredientes);
db = (new DatabaseHelper(this)).getWritableDatabase();
cursor = db.rawQuery(
«SELECT _id, nombre_ingrediente FROM ingredientes», null);

adapter = new CheckboxCursorAdapter( this,
R.layout.listado_ingredientes, cursor, new String[]
{«nombre_ingrediente»}, new int[] {R.id.txt_nombre_ingrediente});

setListAdapter(adapter); }

public class CheckboxCursorAdapter extends SimpleCursorAdapter {

private Cursor c;
private Context context;

public CheckboxCursorAdapter(Context context, int layout, Cursor c,
String[] from, int[] to) {
super(context, layout, c, from, to);
this.c = c;
this.context = context;

}

public View getView(int pos, View inView, ViewGroup parent) {
ViewHolder holder = null;
View v = inView;
if (v == null || !(v.getTag() instanceof ViewHolder)) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.listado_ingredientes, null);
holder = new ViewHolder();
holder.txtingrediente = (TextView) v.findViewById(R.id.txt_nombre_ingrediente);
holder.cBox = (CheckBox) v.findViewById(R.id.chkItem);
holder.cBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

}
});
v.setTag(holder);
}else {

holder = (ViewHolder) v.getTag();
}
this.c.moveToPosition(pos);

holder.txtingrediente.setText(this.c.getString(this.c.getColumnIndex(«nombre_ingrediente»)));

return (v);

}

}

private class ViewHolder {
CheckBox cBox ;
TextView txtingrediente;
}

}

    Alejandro Escario · noviembre 28, 2011 a las 11:28 am

    Estoy muy liado con temas de universidad y trabajo y hasta el día 5 no creo que tenga un respiro para ver qué es lo que falla, lo siento

WolfKey · diciembre 22, 2011 a las 12:40 pm

Mid uda es como puedo saber que checkbox estan chekeados?, he adaptado tu codigo a mi problema, y tengo 3 checkbox por fila. Y un boton al final, me gustaria saber que checkbox estan checkeados cuando hace click en el boton del final para poder tratarlos.

Un saludo

    Alejandro Escario · diciembre 22, 2011 a las 12:45 pm

    Cómo lo has adaptado? con 3 arrays de booleanos o como? en cualquier caso al pulsar el botón, tienes que recorrer el/los array/s y ver por cada fila del array si el booleano que contiene es un true o un false.

neonigma · enero 11, 2012 a las 11:29 am

Buenas, muy buen ejemplo para acelerar el desarrollo de este tipo de formularios, muchísimas gracias 🙂

No sé si se verá bien formateado, pero incluyo la clase Item que te pedían en otro comentario:

public class Item{
private String id;
private String description;

public Item(String pID, String pDesc) {
id = pID;
description = pDesc;
}

public long getID() {
return Integer.valueOf(id);
}

public String getDescription() {
return description;
}

Adicionalmente, modifico un poco tus «getters»:

@Override
public String getItem(int position) {
return items.get(position).getDescription();
}

@Override
public long getItemId(int position) {
return items.get(position).getID();
}

Así obtengo la ID y descripción de la clase Item y no la posición.

Un saludo y gracias de nuevo.

cristian · enero 25, 2012 a las 9:19 pm

justo lo que estaba necesitando, gracias por el aporte
saludos!

tecop · marzo 27, 2012 a las 8:53 am

Hola, me ha servido de mucho tu aporte, me gustaría saber que contiene la clase item que hay que crear o como es para que no de error en los item.
Gracias por adelantado y saludos.

Richard · abril 4, 2012 a las 12:46 pm

Hola,
Justo lo que estaba buscando, pero mi pregunta es: como recupero que checkbox estan checkeados.

gracias de antemano.

    Alejandro Escario · abril 4, 2012 a las 4:06 pm

    Si coges el array de booleanos que utilizamos para mantener el estado de los checkbox, podemos ver si esta chequeado el elemento que esta en esa posición, es decir, si el elemento del vector en la posición 3 tiene un valor igual a true, el elemento en la tercera posición del listview será uno de los que están seleccionados.

    Espero haber resuelto tu duda!

hgramaje · abril 25, 2012 a las 11:33 am

Hola Alejandro!
He seguido tu artículo para mi proyecto y tengo una duda que no consigo resolver.
Yo, en vez de utilizar rows con checkbox, estoy utilizando dos botones «+» y «-» para modificar el contenido de un editText. Resulta que al ejecutar el método «btMas.setOnclickListener…» dentro del «getView» me da un error de puntero nulo. ¿Sabes por qué puede suceder?

Emmanuel Pacheco · marzo 25, 2015 a las 7:06 pm

Hola buen día, estaba tratando de ocupar tu ejemplo para llenar un listview con una consulta a una base de datos. lo hago de la siguiente manera:

Cursor c = conectar.baseDatos.rawQuery(«SELECT * FROM Puntos»,null);

if (c.moveToFirst()) {

do {
punto=new Clase_Puntos(c.getString(0),c.getString(1),c.getString(4));

//listaPuntos.add(punto);

adapPuntos.addItem(new Item(punto.toString()));

} while(c.moveToNext());

}

// adapPuntos.addItem(new Item(String.valueOf(listaPuntos), «item number » + listaPuntos));

lista_Puntos.setAdapter(adapPuntos);

Y si lo hace el problema que tengo es que cuando los agrega a la lista yo quisiera que dijera:

punto1
punto2

tal cual esta en la base de datos sin enmbargo lo imprime asi:

ClipData.Item{T:Punto1}
ClipData.Item{T:Punto2}

podrias ayudarme. Un saludo y que tengas excelente dia.

    Emmanuel Pacheco · marzo 25, 2015 a las 8:49 pm

    Ya resolvi el problema de capa 8 que tenia, XD. bueno solo por si a alguien le sirve. basta con cambiar el tipo del metodo addItem – de tipo Item a String y listo. De igual manera muchas gracias por el aporte amigo.

Los comentarios están cerrados.