sábado, 28 de abril de 2012

Andriod: ListView con Checkbox

Normalmente no suelo escribir este tipo de artículos porque por norma general buscando por internet ya podemos encontrar muchísima información recopilada acerca de ello. Pero en este caso voy hacer una excepción ya que me he dado cuenta de que apenas se encuentran tutoriales sobre el siguiente problema...las listas reutilizables de Android.

Para entrar en materia, explicare cuales son sus efectos y las razones de porque ha sido planteado así.

Supongamos que tenemos un ListView personalizado y en cada item de la lista hay un TextView con un Checkbox. El problema viene dado cuando chequeamos el Checkbox, nos movemos por la lista y al volver, ese checkbox ya no esta chequeado!



Carai! ¿ Y cual sera el problema ? , ¿Que gracia tiene que el usuario le de a una opción y el componente se reinicie solo ?
A groso modo decir que en realidad una lista no tiene cargados todos sus componentes y que cuando nos movemos por la lista, los nuevos componentes que van apareciendo por pantalla han sido creados en ese momento y los que ya han desaparecido los destruye.

¿Y para que los destruye? Imagínate que creas una lista inmensa de resultados, todos ellos han tenido que ser cargados uno por uno aumentando el uso de la CPU y de la memoria, que al final se podría traducir en mayor consumo de batería y pudiéndose dar (a nivel de usuario) que después de tanto procesar la lista, con el primer resultado ya te valga. Así que para evitar eso, se decidió que la mejor forma seria que cargara solo los elementos que pudiera abarcar la pantalla.

Y es por eso (volviendo al problema anterior) que cuando vuelves a la opción donde chequeaste ese item vuelve a estar como en el principio, y es que ha sido creado de zero.


Espero que ahora sepas un poco mas o menos por donde van los tiros.
Ahora si, vamos a picar código.

Para hacerlo de mayor entendimiento he dejado los comentarios dentro del mismo código. También decir que tenéis el código del proyecto al final del articulo para ser descargado.

Creamos la clase Dias.java . Esta clase contendrá la información de cada item de la lista

public class Dias {
private String Dia;
private boolean estado;
//CONSTRUCTOR DE LA CLASE//
public Dias(String Dia, boolean estado) {
this.Dia = Dia;
this.estado = estado;
}

//GETTERS Y SETTERS DE LA CLASE//
public String getDia() {
return Dia;
}

public void setDia(String dia) {
Dia = dia;
}

public boolean isChekeado() {
return estado;
}

public void setChekeado(boolean chekeado) {
estado = chekeado;
}

}


Ahora añadimos en el layout main.xml un nuevo ListView llamado lstLista


<?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="fill_parent"
android:orientation="vertical" >

<ListView
android:id="@+id/lstLista"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</ListView>

</LinearLayout>

Vamos a definir ahora un nuevo layout donde personalizaremos cada item de la lista, voy a llamarle fila.xml y en el hay básicamente un TextView para mostrar el día, un CheckBox para mostrar si esta marcado y varios layouts para que se organicen bien en pantalla


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal" >

<TextView
android:id="@+id/txtDia"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="30dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|right"
android:orientation="vertical" >

<CheckBox
android:id="@+id/chkEstado"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false" />
</LinearLayout>

</LinearLayout>




En el código anterior, coloreado de naranja están  las propiedades necesarias para hacer forzar perder el foco y que no sea clickeable para que el evento setOnClickItemListener pueda ser invocado.


Ahora creamos la clase ListasActivity.java que tendrá la clase principal

import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;

public class ListasActivity extends Activity {

//Se crea un ArrayList de tipo Dias//
ArrayList dias_semana = new ArrayList();
//Se crea una objeto tipo ListView
ListView lstLista;
//Se crea un objeto de tipo AdaptadorDias
AdaptadorDias adaptador;

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.main);

lstLista = (ListView) findViewById(R.id.lstLista);

//Se añaden nuevos Dias en el ArrayList de tipo Dias
dias_semana.add(new Dias("Lunes", false));
dias_semana.add(new Dias("Martes", false));
dias_semana.add(new Dias("Miercoles", false));
dias_semana.add(new Dias("Jueves", false));
dias_semana.add(new Dias("Viernes", false));
dias_semana.add(new Dias("Sabado", false));
dias_semana.add(new Dias("Domingo", false));
dias_semana.add(new Dias("Montag", false));
dias_semana.add(new Dias("Dienstag", false));
dias_semana.add(new Dias("Mittwoch", false));
dias_semana.add(new Dias("Donnertag", false));
dias_semana.add(new Dias("Freitag", false));
dias_semana.add(new Dias("Samstag", false));
dias_semana.add(new Dias("Sonnetag", false));
dias_semana.add(new Dias("Dilluns", false));
dias_semana.add(new Dias("Dimarts", false));
dias_semana.add(new Dias("Dimecres", false));
dias_semana.add(new Dias("Dijous", false));
dias_semana.add(new Dias("Divendres", false));
dias_semana.add(new Dias("Dissabte", false));
dias_semana.add(new Dias("Diumenge", false));

//Se define un nuevo adaptador de tipo AdaptadorDias donde se le pasa como argumentos el contexto de la actividad y el arraylist de los dias
adaptador = new AdaptadorDias(this, dias_semana);

//Se establece el adaptador en la Listview
lstLista.setAdapter(adaptador);

//Esto es mas que nada es a nivel de diseño con el objetivo de crear unas lineas mas anchas entre item y item
lstLista.setDividerHeight(3);

//Se le aplica un Listener donde ira lo que tiene que hacer en caso de que sea pulsado
lstLista.setOnItemClickListener(new OnItemClickListener() {

public void onItemClick(AdapterView arg0, View arg1, int arg2,long arg3) {

//En caso de que la posicion seleccionada gracias a "arg2" sea true que lo cambie a false
if (dias_semana.get(arg2).isChekeado()) {
dias_semana.get(arg2).setChekeado(false);
} else {
//aqui al contrario que la anterior, que lo pase a true.
dias_semana.get(arg2).setChekeado(true);
}
//Se notifica al adaptador de que el ArrayList que tiene asociado ha sufrido cambios (forzando asi a ir al metodo getView())
adaptador.notifyDataSetChanged();

}
});

}

}


//Esta clase extiende de ArrayAdapter para poder personalizarla a nuestro gusto
class AdaptadorDias extends ArrayAdapter {

Activity contexto;
ArrayList dias_semana;

//Constructor del AdaptadorDias donde se le pasaran por parametro el contexto de la aplicacion y el ArrayList de los dias
public AdaptadorDias(Activity context, ArrayList dias_semana) {
//Llamada al constructor de la clase superior donde requiere el contexto, el layout y el arraylist
super(context, R.layout.fila, dias_semana);
this.contexto = context;
this.dias_semana = dias_semana;

}

//Este metodo es el que se encarga de dibujar cada Item de la lista
//y se invoca cada vez que se necesita mostrar un item.
public View getView(int position, View convertView, ViewGroup parent) {
View item = convertView;

//Creamos esta variable para almacen posteriormente en el la vista que ha dibujado
VistaItem vistaitem;

//Si se decide que no existe una vista reutilizable para el proximo item entra en la condicion.
//De este modo tambien ahorramos tener que volver a generar vistas
if (item == null) {

//Obtenemos una referencia de Inflater para poder inflar el diseño
LayoutInflater inflador = contexto.getLayoutInflater();

//Se le define a la vista (item) el tipo de diseño que tiene que tener
item = inflador.inflate(R.layout.fila, null);

//Creamos un nuevo vistaitem que se almacenara en el tag de la vista
vistaitem = new VistaItem();

//Almacenamos en el objeto la referencia del TextView buscandolo por ID
vistaitem.nombre = (TextView) item.findViewById(R.id.txtDia);

//tambien almacenamos en el objeto la referencia del CheckBox buscandolo por ID
vistaitem.chkEstado = (CheckBox) item.findViewById(R.id.chkEstado);

//Ahora si, guardamos en el tag de la vista el objeto vistaitem
item.setTag(vistaitem);

} else {
//En caso de que la vista sea ya reutilizable se recupera el objeto VistaItem almacenada en su tag
vistaitem = (VistaItem) item.getTag();
}

//Se cargan los datos desde el ArrayList
vistaitem.nombre.setText(dias_semana.get(position).getDia());
vistaitem.chkEstado.setChecked(dias_semana.get(position).isChekeado());
//Se devuelve ya la vista nueva o reutilizada que ha sido dibujada
return (item);
}


//Esta clase se usa para almacenar el TextView y el CheckBox de una vista y es donde esta el "truco" para que las vistas se guarden
static class VistaItem {
TextView nombre;
CheckBox chkEstado;

}

}


Descargate aquí el código entero del proyecto y míralo con mas calma si lo crees necesario. (Ha sido programado en android 4.0.3, pero si lo ves necesario cámbialo para ejecutarlo en tu correspondiente versión)




13 comentarios:

  1. Creo que olvidaste poner el enlace...

    ResponderEliminar
  2. Hola, me sirvio mucho el aporte, pero tengo una duda. En tu ejemplo como hago para recorrer la lista y crear una arraylist con los dias de la semana que estan seleccionados?

    ResponderEliminar
  3. Muchas gracias, me ha servido de gran ayuda

    ResponderEliminar
  4. Esto funciona perfecto:
    ListView listaDePrevia = (ListView) findViewById(R.id.listadeitemsdeprevia);
    int nItems = listaDePrevia.getChildCount();
    CheckBox lMarcado;
    String cMensaje = "Marcados\n\n",cCodigoMarcado;
    if(true){
    for (int i = 0; i < nItems; i++) {

    lMarcado = (CheckBox) listaDePrevia.getChildAt(i).findViewById(R.id.checkBoxMarcado);
    if (lMarcado.isChecked()) {

    cCodigoMarcado = ((TextView) listaDePrevia.getChildAt(i).findViewById(R.id.textViewCodigo)).getText().toString();

    cMensaje = cMensaje + cCodigoMarcado + "\n";
    }
    }
    }

    if(cMensaje.length()>0){
    JOptionPane.alert("CODIGOS Eliminados",cMensaje).show() ;
    }

    ResponderEliminar
    Respuestas
    1. Muchas gracias me has ayudado bastante

      Eliminar
    2. Tengo problemas a usar su codigo, ya que lo he tenido que modificar un poco, pongo el codigo y cuando podais le echais una vista y me comentais el fallo que tengo ya que el mensaje de error es el siguiente:
      java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View.findViewById(int)' on a null object reference

      apuntando a la linea lMarcado=(CheckBox)lstLista.getChildAt(i).findViewById(R.id.chkEstado); ya que siempre como maximo me recorreo 6 de cada fragment.
      El codigo es el siguiente:
      // Definimos un objeto del activity VentanaEditarUsuario
      final VentanaEditarUsuario activity2 = ((VentanaEditarUsuario) getActivity());

      //Obtenemos el id_usuario que hemos elegido
      String[] elusuario = new String[] {activity2.getMyData()};

      //Llamamos a la funcion actualizar usuario.
      actualizarUsuario(elusuario[0],nombre,apellidos,correo,telefono);

      //Llamamos a la funcion eliminar todas las relaciones de usuario con ingrediente del usuario que estamos
      //editando.
      eliminar_idusuario_idingrediente(elusuario[0]);

      //Procedemos a introducir las tuplas con las que se relaciona el usuario con los ingredientes.
      //Por lo que recorredmos los 16 fragments de la Ventana Registro Usuario.
      for(int j=5;j<16;j++)
      {
      //Vamos recorrido la lista de cada fragments
      lstLista = (ListView) activity2.fragments.get(j).getView().findViewById(R.id.lstLista);

      int nItems = lstLista.getCount();
      System.out.println(nItems);

      System.out.println(j+" de "+16);

      //Introducimos los id_ingredientes de los que esten marcados
      for (int i=0;i<nItems-1;i++)
      {
      System.out.println(i+"/"+nItems);
      System.out.println(lstLista.getCount());
      lMarcado=(CheckBox)lstLista.getChildAt(i).findViewById(R.id.chkEstado);
      System.out.println(lMarcado.isChecked());
      if (lMarcado.isChecked())
      {

      String id=((TextView)(lstLista.getChildAt(i).findViewById(R.id.idingrediente))).getText().toString();

      insertar_idusuario_idingrediente(elusuario[0].toString(),id.toString());

      System.out.println(elusuario[0]+" "+id.toString());

      //Toast.makeText(activity, ((TextView)(lstLista.getChildAt(i).findViewById(R.id.idingrediente))).getText().toString(), Toast.LENGTH_SHORT).show();
      }
      }
      }

      Toast.makeText(getActivity().getBaseContext(), "!Usuarios Modificado Correctamente¡", Toast.LENGTH_LONG).show();

      Eliminar
  5. hola alguien sabe como mandar los datos selleccionas en los checkbox por correo????

    ResponderEliminar
  6. Quedo muy bien, pero por favor que nuevo arreglo habría que hacer al codigo de Guillermo Ledantes para recorrer todos los checkboxes sin perder aquellos que estuviesen fuera de la pantalla cuando la lista es muy grande

    ResponderEliminar
    Respuestas
    1. Saludos David Ortiz,

      Una opción seria volver acceder al adaptador y desde allí tendrías la lista de todos los items.

      Obtén primer las Views (vistas) y desde allí podrías volver acceder a la función getTag() y recuperar lo que guardaste.

      Eliminar
    2. Teneis alguna respuesta sobre este tema, ya que a mi me pasa lo mismo.

      Eliminar
  7. tengo un arraylist y cuando marco un checkbox y cierro la aplicacion ya no esta marcado. ?????como puedo hacer que siga marcado cuando abro nuevamente la aplicacion????????????

    ResponderEliminar