ListView (Android)

TEORÍA

La etiqueta ListView es un contenedor de vistas (un ViewGroup) que permite la creación rápida de una lista vertical de elementos en pantalla con un scroll automático que envuelve a la ListView (el desarrollador no tiene que definir la ScrollView).

La ListView puede ser poblada dinámicamente desde código usando la clase «ArrayAdapter» o bien estáticamente usando un «string-array» almacenado en un archivo XML cuyo nodo raíz sea «resources» (por convenio almacenado en «res/values/arrays.xml«). El ArrayAdapter es una clase Java que permite la «traducción» directa de un conjunto de datos a cada uno de los items que poblarán la ListView (por ejemplo, el ArrayAdapter puede utilizar un array de strings y convertirlos en TextViews para poblar el nodo ListView).

Las ListView van a contener items (cada uno de los elementos de la lista) y estos pueden ser «no seleccionables», «seleccionable único» o ser «multi-seleccionables»; para ello se definirá su modo de selección en el atributo «android:choiceMode» del ListView. Si la ListView tiene un modo de selección única (singleChoice), sólo se marcará el último ítem seleccionado de la lista; si no es seleccionable (none), no marcará ningún ítem; y si es multi-seleccionable (multipleChoice) marcará al ítem si es pulsado una vez y lo desmarcará al volver a pulsarlo.

Marcar un ítem, en este contexto, quiere decir almacenar en memoria un booleano en el objeto ListView que diga si el elemento de la lista en la posición X está seleccionado actualmente. No tiene nada que ver con la interfaz gráfica de la lista; es decir, un ítem puede ser seleccionado en memoria pero no mostrar ningún feedback al usuario que le indique que haya seleccionado dicho ítem. Este feedback deberá darlo el Layout que se le asigne a las filas/ítems del ListView desde código Java (mediante el ArrayAdapter). Por ejemplo:

Una ListView con selección múltiple que no le da feedback al usuario:

Aunque seleccione varios elementos, el usuario no se va a enterar de ello porque el Layout que define el ítem de la lista no cambia al ser pulsado (cada fila es un Layout que sólo se compone de un TextView). Pero en memoria sí se almacenan los elementos que se han seleccionado.

Una ListView con selección múltiple que le da feedback al usuario:

Sin embargo, en este ejemplo, el usuario sabe qué elementos ha seleccionado porque el Layout que define el ítem de la fila cambia al ser pulsado (tiene un checkbox que indica que ha sido seleccionado).

Entonces, el modo de selección (choiceMode) de la ListView, sólo define el comportamiento de cómo el objeto ListView almacena los elementos seleccionados en memoria, en ningún caso crea un Layout adecuado según el modo de selección, esto último queda a cargo del programador.

android:choiceMode

El ListView almacena los elementos seleccionados en memoria según el valor que se le haya dado a este atributo de la siguiente manera:

  • none: no almacena ninguna selección aunque el usuario pulse en el ítem.
  • singleMode: almacena la última selección.
  • multipleChoice: almacena la selección si se pulsa una vez y la des-almacena si se vuelve a pulsar.

Los Layouts adecuados para cada tipo de «choiceMode» del ListView (y que vienen por defecto en el SDK de Android) son:

none => simple_list_item_1

Cada ítem de la ListView se compone de un TextView.

singleChoice=> simple_list_item_single_choice

Cada ítem de la ListView se compone de un RadioButton con un texto asociado (en realidad es un sólo nodo de tipo CheckedTextView cuyo elemento «checkable» tiene forma de RadioButton).

multipleChoice => simple_list_item_multiple_choice

Cada ítem de la ListView se compone de un CheckBox con un texto asociado (en realidad es un sólo nodo de tipo CheckedTextView cuyo elemento «checkable» tiene forma de CheckBox).

A parte de estos Layouts predefinidos por el sistema, se pueden definir otros (uno por ejemplo que contenga, además de un texto, una imagen), en ese caso el ArrayAdapter por defecto no será valido, pues no sabe cómo transformar una secuencia de datos que traigan texto e imágenes, es por ello que debe definirse un tipo de ArrayAdapter especial (heredando de este) por cada Layout que defina el desarrollador.

Estos Layouts sólamente se pueden asignar a través de código Android, teniendo por defecto las ListViews el Layout correspondiente al «simple_list_item_1«.

Colores

Cuando se pulsa en una fila de la lista, se muestran siempre dos colores: uno cuando está siendo pulsado y otro cuando se ha levantado el dedo después de la pulsación. Para definir estos colores se debe hacer referencia a un elemento drawable (almacenado en la carpeta «res/drawable«) en el atributo «android:listSelector«; o bien definir un color, en cuyo caso sólo se estará definiendo el color para el momento en que se levanta el dedo tras la pulsación.

PRÁCTICA

Declaración en XML

La etiqueta ListView estará vacía (no tiene hijos).

<ListView 
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  ... />

Tiene 5 atributos importantes:

  • android:entries: si se desean indicar las entradas estáticas desde un nodo «string-array», es aquí donde se pondrá la referencia a las entradas que, por convenio, se almacenan en «res/values/arrays.xml«. Si no se desean entradas estáticas se omite este atributo.
<!-- res/values/arrays.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="mis_items_de_list_view">
        <item>Opción 1</item>
        <item>Opción 2</item>
    </string-array>
</resources>
<!-- mi_activity_layout.xml -->
<ListView android:entries="@array/mis_items_de_list_view" ... />
  • android:choiceMode: el modo de selección de los elementos de la lista. Pueden ser:
    • no-seleccionables: «none«, ningún ítem puede ser marcado.
    • Única selección: «singleChoice«, sólo un elemento de la lista puede ser marcado.
    • Multi-selección: «multipleChoice«, se pueden marcar varios elementos de la lista.
<ListView android:choiceMode="none" ... />
<!-- ó -->
<ListView android:choiceMode="singleChoice" ... />
<!-- ó -->
<ListView android:choiceMode="multipleChoice" ... />
  • android:divider: color (ó elemento drawable) de la línea divisoria que hay entre los ítems de la lista. Es opcional, por defecto se pondrá un divisor discreto.
  • android:dividerHeight: grosor de la línea divisoria. Es opcional, por defecto se pondrá un divisor discreto.
<ListView 
    android:divider="#000000"
    android:dividerHeight="10sp" />
  • android:listSelector: color (o elemento drawable) que define los colores que deben tener cada uno de los elementos de la lista cuando están siendo pulsados o cuando hayan sido pulsados:
<ListView
  ...
  android:listSelector="@drawable/coloresItemLista"/>

<!-- ó definir un color directamente -->

<ListView
  ...
  android:listSelector="@android:color/holo_green_light"/>

Si el atributo listSelector se define como un elemento drawable, este puede definir varios items con un atributo concreto que defina si el color definido es para el estado pulsado o pulsando:

<!-- res/drawable/coloresItemLista -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <!-- el elemento está siendo pulsado -->
  <item android:state_pressed="true" android:drawable="@android:color/holo_blue_dark"/>
  <!-- la pulsación del elemento ha terminado -->
  <item android:drawable="@android:color/holo_green_light"/>
</selector>

Este sería el efecto final.

Manipulación dinámica del ListView

Poblar el ListView

Requiere un ArrayList de Strings que contengan los datos si deseamos crear una ListView que contenga sólo etiquetas de texto (TextViews) o etiquetas de texto con un elemento «chequeable» (CheckedTextViews).

Se le añaden los datos al ArrayList de Strings y se le pasan como parámetro al ArrayAdapter, indicando con qué tipo de Layout debe ser representada cada fila de la lista (por ejemplo simple_list_item_1).

public class MiActivity extends AppCompatActivity{
  ListView miListView;
  protected void onCreate(Bundle savedInstanceState) {
    //...
    miListView = (ListView) findViewById(R.id.miListView);
    ArrayList<String> itemsDeMiLista = new ArrayList<>();
    itemsDeMiLista.add("Primer elemento");
    itemsDeMiLista.add("Segundo elemento");
    //...
    miListView.setAdapter(
      new ArrayAdapter(
        this, //contexto
        //Indicamos el tipo de Layout con el que representar
        //cada fila de la ListView
        android.R.layout.simple_list_item_1, 
        itemsDeMiLista
      )
    );
  }
}

El ArrayAdapter puede ser modificado dinámicamente, útil para listas que pueden cambiar de Layout para sus ítems (por ejemplo, una lista que en inicio no es seleccionable pero que, al producirse una pulsación prolongada se vuelva seleccionable).

miListViewAdapterMultiple = new ArrayAdapter(
	this, 
	android.R.layout.simple_list_item_multiple_choice, 
	miListView);

miListViewAdapterNone = new ArrayAdapter(
	this, 
	android.R.layout.simple_list_item_1, 
	miListView);

clientsNotSentContainer.setAdapter(miListViewAdapterNone);
// Luego...
clientsNotSentContainer.setAdapter(miListViewAdapterMultiple);
EventListeners

Se puede definir un EventListener para cuando un elemento de la lista haya sido pulsado (OnItemClickListener) y otro para cuando un elemento haya sido pulsado prolongadamente (OnItemLongClickListener).

miListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position,long arg3) {
        //view es el ítem de la lista pulsado
    }
});

miListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        //view es el ítem de la lista pulsado de forma prolongada
        //return true provoca que el OnItemClickListener NO se ejecute
        //return false provoca que se ejecute
        return true;
    }
});
Elementos seleccionados

Se puede modificar y recuperar el método de selección (choiceMode) de la ListView dinámicamente:

miListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 

if(ListView.CHOICE_MODE_NONE == miListView.getChoiceMode()){
	//...
}

Si el método de selección es múltiple o único, se puede saber qué elemento(s) ha(n) sido seleccionado(s).

NOTA: El método de selección «none» no almacena ninguna selección.

for (int i = 0; i < miListView.getCount(); i++) {
  if(miListView.isItemChecked(position)){
    //...
  }
}

Si es un selector múltiple, el elemento pulsado por el usuario puede estar siendo seleccionado o deseleccionado, así que en los EventListener podemos saber en qué estado se encuentra:

public void onItemClick(AdapterView<?> parent, View view, int position,long arg3) {
	if( ((CheckedTextView)view).isChecked() ){
		//...
	}
	//ó
	if(miListView.isItemChecked(position)){
		//...
	}
}

NOTA: es preferible usar el método «ListView::isItemChecked()» para evitar errores de casting, dependiendo del Layout usado para representar al ítem, el elemento será de un tipo u otro.

Se puede saber cuántos ítems hay seleccionados actualmente:

miListView.getCheckedItemCount();

Reiniciar la cuenta de los elementos seleccionados (checked) de la lista. Puede ser necesario si quieres volver a poblar la misma lista (aunque la pongas a NULL y vuelvas a seguir todo el proceso de poblar la ListView, el contador puede no reiniciarse automáticamente, debe hacerse explícitamente).

miListView.clearChoices();
Iterar sobre la lista

Se pueden recorrer los elementos de la lista:

for (int i = 0; i < miListView.getCount(); i++) {
    View v = miListView.getChildAt(i);
    //...
}

Lista seleccionable/no-seleccionable

Es muy habitual encontrar en aplicaciones Android la típica experiencia de usuario en la cual una lista de elementos primero no pueden ser seleccionados (al ser pulsados te llevan a otra Activity o te muestran detalles) pero que, al ser pulsados prolongadamente, se vuelva una lista de elementos seleccionable para ejecutar acciones en lote.

Este es un ejemplo de cómo realizar tal lista:

public class MiActivity extends AppCompatActivity{
    ListView miListView;
    ArrayAdapter miListViewAdapterMultiple;
    ArrayAdapter miListViewAdapterNone;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        miListView = (ListView) findViewById(R.id.miLista);
        ArrayList<String> miListView = new ArrayList<>();
        miListView.add("blabla1");
        miListView.add("blabla2");
        miListView.add("blabla3");
        miListView.add("blabla4");
    
        miListViewAdapterMultiple = new ArrayAdapter(
            this, 
            android.R.layout.simple_list_item_multiple_choice, 
            miListView);
        miListViewAdapterNone = new ArrayAdapter(
            this, 
            android.R.layout.simple_list_item_1, 
            miListView);
        miListView.setAdapter(miListViewAdapterNone);
        miListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
        miListView.setOnItemClickListener(
            new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(
                AdapterView<?> parent, 
                View view, 
                int position,
                long arg3) 
            {
                if(ListView.CHOICE_MODE_NONE == miListView.getChoiceMode()){
                    //Mostrar detalles del elemento pulsado
                    //ó realizar un Intent para abrir otra Activity
                }
                else if(ListView.CHOICE_MODE_MULTIPLE == miListView.getChoiceMode()) { 
                    //Si no hay items seleccionados vuelve al modo no selección
                    if(0 == miListView.getCheckedItemCount()){
                        miListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
                        miListView.setAdapter(miListViewAdapterNone);
                    }
                }
            }
        });
        miListView.setOnItemLongClickListener(
            new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(
                AdapterView<?> parent, 
                View view, 
                int position, 
                long id) 
            {
                if(ListView.CHOICE_MODE_MULTIPLE == miListView.getChoiceMode()){
                  // Si es una pulsación prolongada pero ya está en modo 
                  // selección múltiple, ejecuta el evento "OnClick"
                    return false;
                }
                else {
                  // En otro caso, cambiar a modo "selección múltiple"
                  // y seleccionar este elemento
                    miListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
                    miListView.setAdapter(miListViewAdapterMultiple);
                    miListView.setItemChecked(position, true);
                    return true;
                }
            }
        });
    }
    @Override
    public void onBackPressed() {
    // Cuando se pulsa el botón "back" se deseleccionan
      // los elementos si está en modo selección múltiple
      if(ListView.CHOICE_MODE_MULTIPLE == miListView.getChoiceMode()) {
          miListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
          miListView.setAdapter(miListViewAdapterNone);
      } else {
          super.onBackPressed();
      }
  }
}

Deja una respuesta