La vista ListView

Una vista ListView visualiza una lista deslizable verticalmente de varios elementos, donde cada elemento puede definirse como un Layout .Su utilización es algo compleja, pero muy potente. Un ejemplo lo podemos ver en la siguiente figura:

 

Definir un ListView conlleva los siguientes cuatro pasos:

  • Diseñar un Layout que lo contenga al ListView
  • Diseñar un Layout individual que se repetirá en la lista
  • Implementar una actividad que lo visualice el Layout con el ListView
  • Personalizar cada una de los Layouts individuales según nuestros datos  

Veamos estos pasos con más detalle:

Para utilizar un ListView dentro de un Layout puedes de usar la siguiente estructura:

<FrameLayout>
       <ListView
       android:id="@android:id/list"... />
       <TextView
              android:id="@android:id/empty"
              ... />
</FrameLayout>

Donde tenemos un FrameLayout que permite visualizar dos posibles elementos, uno u otro, pero no los dos simultáneamente. El primero es el ListView que se visualizará cuando haya algún elemento en la lista. El segundo puede ser cualquier tipo de vista y se visualizará cuando no existan elementos en la lista. El sistema controla la visibilidad de forma automática, solo has de tener cuidado de identificar cada uno de los elementos con el valor exacto que se muestra.

NOTA: Recuerda que para crear nuevos identificadores debes utilizar la expresión"@+id/nombre_identificador". El carácter @ significa que se trata de un identificador de recurso que se definirá en la clase R.java. El carácter + significa que el recurso ha de ser creado en este momento. En este caso hemos utilizado identificadores definidos en el sistema (es decir @android: significa que es un recurso definido en la clase android.R.java).

Una vez creado el Layout que contiene el ListView tendremos que visualizarlo en una actividad. Para este propósito utilizaremos un  tipo de actividad especial,  ListActivity.

También tendremos que indicar al sistema cada uno de los Layouts individuales que contendrá el ListView. Esto lo haremos llamando al método setListAdapter(). Existen varias alternativas con diferentes grados de dificultad. Para una mejor conprensión iremos mostrando tres ejemplos de uso de setListAdapter(), de más sencillo a más complejo.

Las capturas anteriores muestran los tres ListView que vamos construir. El de la izquierda se limita a mostrar una lista de Strings. El del centro visualiza una lista de un Layout diseñado por nosotros. Aunque este Layout tiene varios componentes (una imagen y dos textos), solo cambiamos uno de los textos. En el último ejemplo cambiaremos también la imagen de cada elemento.

video[Tutorial] Uso de ListView

 

Un ListView que visualiza una lista de Strings

Ejercicio paso a paso: Un ListView que visualiza una lista de Strings

1.     El Layout que utilizaremos en Asteroides para mostrar las puntuaciones se llamará puntuaciones.xml. En el se incluye una vista ListView. Crea el Layout con el siguiente código:

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Puntuaciones"
       android:gravity="center"
       android:layout_margin="10px"
       android:textSize="10pt""/>
<FrameLayout
       android:layout_width="match_parent"
       android:layout_height="0dip"
       android:layout_weight="1">
       <ListView
              android:id="@android:id/list"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:drawSelectorOnTop="false" />
       <TextView
              android:id="@android:id/empty"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:text="No hay puntuaciones" />
</FrameLayout>
</LinearLayout>


2.     Necesitamos ahora crear la actividad Puntuaciones para visualizar el Layout anterior. Crea una nueva clase en tu proyecto e introduce el siguiente código:

public class Puntuaciones extends ListActivity {
    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.puntuaciones);
        setListAdapter(new ArrayAdapter(this,
                               android.R.layout.simple_list_item_1,
                               Asteroides.almacen.listaPuntuaciones(10)));
    }
}

Toda actividad que vaya a visualizar un ListView ha de heredar de ListActivity. Además, ha de llamar al método setListAdapter() para indicar el adaptador con la lista de elementos a visualizar. En el ejemplo se ha utilizado una de la posibilidades más sencillas, para crear un adaptador, usar la clase ArrayAdapter<clase>. Un ArrayAdapter crea las vistas del ListView a partir de los datos almacenados en un array. Puedes utilizar un array que contenga datos de cualquier clase, no tienes más que indicar en <Clase> la clase deseada. En este caso se utiliza de un array de String[1]. El constructor de ArrayAdapter<clase> tiene tres parámetros: El primer parámetro es un Context con información sobre el entorno de la aplicación. Utilizaremos como contexto la misma actividad que hace la llamada. El segundo parámetro es un Layout, utilizado para representar cada elemento de la lista. En este ejemplo, en lugar de definir uno nuevo, utilizaremos una ya definido en el sistema. El último parámetro es un array con los strings a mostrar. Para ello, llamamos al método listaPuntuaciones() que nos devuelve esta lista del objeto estático almacen de la clase Asteroides.

3.     Recuerda que toda nueva actividad ha de ser registrada en AndroidManifest.xml.

4.     Prueba si funcionan las modificaciones introducidas.

Un ListView que visualiza Layouts personalizados

Vamos a personalizar el ListView anterior para que cada elemento de la lista sea un Layoutdefinido por nosotros. Para ello sigue los siguientes pasos:

Ejercicio paso a paso: Un ListView que visualiza layouts personalizados

1.     Reemplaza la clase anterior por:

 public class Puntuaciones extends ListActivity {
    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.puntuaciones);
        setListAdapter(
                 new ArrayAdapter(this,
                       R.layout.elemento_lista,
                       R.id.titulo,
                       Asteroides.almacen.listaPuntuaciones(10)));
    }
}

Como hemos explicado, la clase ArrayAdapter<String> permite insertar los datos desde un array de String en nuestro ListView. En este ejemplo se utiliza un constructor con cuatro parámetros:

this: es el contexto, con información sobre el entorno de la aplicación.

R.layout.elemento_lista: es una referencia de recurso a la vista que será utilizada repetidas veces para formar la lista. Se define a continuación.

R.id. titulo:  identifica un id de la vista anterior que ha de ser un TextView. Su texto será reemplazado por el que se indica en el siguiente parámetro.

Asteroides.almacen.listaPuntuaciones(10)vector de String con los textos que serán visualizados en cada uno de los TextView. Esta lista es obtenida accediendo a la clase Asteroides a su variable estática  almacen llamando a su método listaPuntuaciones().

2.     Ahora hemos de definir el Layout que representará cada uno de los elementos de la lista.   Crea el fichero res/Layout/elemento_lista.xml con el siguiente código:

<RelativeLayout  
       xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="?android:attr/listPreferredItemHeight">
       <ImageView android:id="@+id/icono"
             android:layout_width="?android:attr/listPreferredItemHeight"
             android:layout_height="match_parent"
             android:layout_alignParentLeft="true"
             android:src="@drawable/asteroide2"/>
       <TextView android:id="@+id/titulo"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_toRightOf="@id/icono"
             android:layout_alignParentTop="true"
             android:textAppearance="?android:attr/textAppearanceLarge"
             android:singleLine="true" />
       <TextView android:id="@+id/subtitulo"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:text="Otro Texto"
             android:layout_toRightOf="@id/icono"
             android:layout_below="@id/titulo"
             android:layout_alignParentBottom="true"
             android:gravity="center"/>
</RelativeLayout>

Este Layout representa una imagen a la izquierda con dos textos a la derecha, uno de mayor tamaño en la parte superior. Para combinar estos elementos se ha escogido un RelativeLayout, donde el alto se establece a partir de un parámetro de configuración del sistema ?android:attr/listPreferredItemHeight. El primer elemento que contiene es un ImageView alineado a la izquierda. Su alto es la misma que el contenedor (match_parent) mientras que el ancho se establece con el mismo parámetro que el alto del contenedor. Por lo tanto la imagen será cuadrada.

3. Puedes descargarte las imágenes utilizadas en la aplicación Asteroides de www.androidcurso.com. En el menú El gran libro de Android / Ficheros usados en ejercicios dentro de Graficos.zip.  Copia el fichero asteriode1.png, asteriode2.png y  asteriode3.png a la carpeta res/drawable.

4.     Ejecuta la aplicación y verifica el resultado.

Un ListView con nuestro propio adaptador

En el ejercicio anterior hemos visto como podíamos asociar un Layout definido por nosotros al ListView y personalizar uno de sus campos. Si queremos algo más adaptable,por ejemplo cambiar varios campos, tendremos que escribir nuestro propio adaptador  extendiendo la clase BaseAdapter. En esta clase habrá que sobreescribir los siguientes cuatro métodos:

View getView(int position, View convertView, ViewGroup parent)

Este método ha de construir un nuevo objeto View que será visualizado en la   posición position . Opcionalmente podemos partir de una vista base convertView para generar más rápido  este objeto. El último parámetro corresponde al contenedor de vistas donde el objeto va a ser añadido.

int getCount()

Devuelve el número de elementos de la lista. 

Object getItem(int position)

Devuelve el elemento en una determinada posición de la lista.

long getItemId(int position)

Devuelve el identificador de fila de una determinada posición de la lista.

Veamos un ejemplo:

Ejercicio paso a paso: Un ListView con nuestro propio adaptador

1. Crea la clase MiAdaptador.java en el proyecto con el siguiente código:

public class MiAdaptador extends BaseAdapter {
       private final Activity actividad;
       private final Vector lista;
 
       public MiAdaptador(Activity actividad, Vector lista) {
             super();
             this.actividad = actividad;
             this.lista = lista;
       }

       public View getView(int position, View convertView, 
                         ViewGroup parent) {
             LayoutInflater inflater = actividad.getLayoutInflater();
             View view = inflater.inflate(R.layout.elemento_lista, null, true);
             TextView textView =(TextView)view.findViewById(R.id.titulo);
             textView.setText(lista.elementAt(position));
             ImageView imageView=(ImageView)view.findViewById(R.id.icono);
             switch (Math.round((float)Math.random()*3)){
             case 0:
                    imageView.setImageResource(R.drawable.asteroide1);
                    break;
             case 1:
                    imageView.setImageResource(R.drawable.asteroide2);
                    break;
             default:
                    imageView.setImageResource(R.drawable.asteroide3);
                    break;
             }
             return view;
       }

       public int getCount() {
             return lista.size();
       }

       public Object getItem(int arg0) {
             return lista.elementAt(arg0);
       }

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

2. En el constructor de la clase se indica la actividad donde se ejecutará y la lista de datos a visualizar. El método más importante de esta clase es getView() el cual tiene que construir los diferentes Layouts que serán añadidos en la lista. Comenzamos construyendo un objeto View a partir del código xml definido en elemento_lista.xml. Este trabajo se realiza por medio de la clase LayoutInflater.Luego, se modifica el texto de uno de los TextView según el array que se pasó en el constructor. Finalmente, se obtiene un número al azar (Math.round()) y se asigna uno de los tres gráficos de forma aleatoria.

3. Reemplaza en la clase Puntuaciones la llamada al constructor de ArrayAdapter<String> por:

   setListAdapter(new MiAdaptador(this,
             Asteroides.almacen.listaPuntuaciones(10)));

4. Ejecuta la aplicacióny verifica el resultado.

Ejercicio paso a paso: Un List View en Mis Lugares

 En este ejercicio vamos a crear un ListViewen la actividad principal de Mis Lugares para que nos muestre una lista con los lugares disponibles:

1.     Crea un nuevo layout que se llame activity_main.xml con el siguiente código:

<?xmlversion="1.0" encoding="utf-8"?>

<FrameLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
   <ListView
      android:id="@android:id/list"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:drawSelectorOnTop="false" />
   <TextView
      android:id="@android:id/empty"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:text="No hay lugares" />
</FrameLayout>

2.     En la actividad MainActivityelimina el código tachado y añade el subrayado:

publicclass MainActivity extends Activity ListActivity {

   public AdaptadorLugares adaptador;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.edicion_lugar);
      setContentView(R.layout.activity_main);
      adaptador= new AdaptadorLugares(this);
      setListAdapter(adaptador);
   }
    …

Una actividad que vaya a visualizar un ListView ha de heredar de ListActivity. Además, ha de llamar al método setListAdapter() para indicar el adaptador con la lista de elementos a visualizar. En el ejemplo se ha utilizado una de la posibilidades más compleja para crear un adaptador: usar una clase definida por nosotros (AdaptadorLugares). Esta clase se muestra al final del ejercicio. El código añadido en onCreate() se ha explicado en ejercicios anteriores.

3.     Ahora hemos de definir el layout que representará cada uno de los elementos de la lista. Crea el fichero res/layout/elemento_lista.xml con el siguiente código:

<?xmlversion="1.0" encoding="utf-8"?>
<RelativeLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="?android:attr/listPreferredItemHeight">
       <ImageView android:id="@+id/foto"
             android:layout_width="?android:attr/listPreferredItemHeight"
             android:layout_height="?android:attr/listPreferredItemHeight"
             android:layout_alignParentLeft="true"
             android:contentDescription="fotografía"
             android:src="@drawable/bar"/>
       <TextView android:id="@+id/nombre"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="Nombres del lugar"
             android:layout_toRightOf="@id/foto"
             android:layout_alignParentTop="true"
             android:textAppearance="?android:attr/textAppearanceMedium"
             android:singleLine="true" />
       <TextView
           android:id="@+id/direccion"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_below="@id/nombre"
           android:layout_toRightOf="@+id/foto"
           android:gravity="center"
           android:singleLine="true"
            android:text="dirección del lugar"/>
       <RatingBar
           android:id="@+id/valoracion"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_alignParentBottom="true"
           android:layout_below="@id/direccion"
           android:layout_toRightOf="@+id/foto"
           style="?android:attr/ratingBarStyleSmall"
           android:isIndicator="true"
           android:rating="3"/>
</RelativeLayout>

Para combinar las vistas se ha escogido un RelativeLayout. Su altura se establece a partir de un parámetro de configuración del sistema ?android:attr/listPreferredItemHeight (altura preferida para ítem de lista). El primer elemento que contiene es un ImageView alineado a la izquierda. Su altura es la misma que el contenedor (match_parent) y su anchura se establece con el mismo parámetro que la altura del contenedor. Por lo tanto, la imagen será cuadrada. A la derecha se muestran dos textos. En el texto de mayor tamaño se visualizará el nombre del lugar y en el de menor tamaño, la dirección. Bajo estos textos se ha incluido un RatingBar.

4.     Crea la clase AdaptadorLugares.java en MisLugares con el siguiente código:

public class AdaptadorLugares extends BaseAdapter {
    private LayoutInflater inflador; // Crea Layouts a partir del XML
    TextView nombre, direccion;
    ImageView foto;
    RatingBar valoracion; 

    public AdaptadorLugares(Context contexto) {
        inflador = (LayoutInflater) contexto
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public View getView(int posicion, View vistaReciclada,
                                            ViewGroup padre) {
        Lugar lugar = Lugares.elemento(posicion);
        if (vistaReciclada == null) {
            vistaReciclada =
                   inflador.inflate(R.layout.elemento_lista, null);
        }

        nombre = (TextView) vistaReciclada.findViewById(R.id.nombre);
        direccion =
                 (TextView) vistaReciclada.findViewById(R.id.direccion);
        foto = (ImageView) vistaReciclada.findViewById(R.id.foto);
        valoracion =
                 (RatingBar) vistaReciclada.findViewById(R.id.valoracion);
        nombre.setText(lugar.getNombre());
        direccion.setText(lugar.getDireccion());
        int id = R.drawable.otros;
        switch(lugar.getTipo()) {
             case RESTAURANTE:id = R.drawable.restaurante; break;
             case BAR:    id = R.drawable.bar;     break;
             case COPAS:   id = R.drawable.copas;    break;
             caseESPECTACULO:id = R.drawable.espectaculos; break;
             caseHOTEL:   id = R.drawable.hotel;    break;
             case COMPRAS:  id = R.drawable.compras;   break;
             case EDUCACION: id = R.drawable.educacion;  break;
             case DEPORTE:  id = R.drawable.deporte;   break;
             case NATURALEZA: id = R.drawable.naturaleza; break;
             case GASOLINERA: id = R.drawable.gasolinera; break;
        }

        foto.setImageResource(id);
        foto.setScaleType(ImageView.ScaleType.FIT_END);
        valoracion.setRating(lugar.getValoracion());
        return vistaReciclada;
    }

    public int getCount() {
        return Lugares.size();
    }

    public Object getItem(int posicion) {
        return Lugares.elemento(posicion);
    }

    public long getItemId(int posicion) {
        return posicion;
    }
}

En el constructor de la clase creamos un inflater en el objeto inflador. Un  inflater es una herramienta que nos permite crear un objeto Java a partir de un fichero XML que lo describe. En el ejemplo creamos un inflater para layouts.

En esta clase, el método más importante es getView(), podría ser una vista totalmente diferente de las demás., que usa el sistema para pedir cada uno de los elementos a insertar. Cuando se llame a getView(), podría ser una vista totalmente diferente de las demás., se nos indicarán tres parámetros: la posición del elemento a insertar, una vista reciclada y el layout contenedor donde se insertará el elemento. Este método ha de devolver una vista con la información adecuada del elemento a insertar.

El parámetro vistaReciclada se utiliza para mejorar el rendimiento en la creación de vistas. Para la primera llamada a getView(), podría ser una vista totalmente diferente de las demás., este parámetro será nulo y tendremos que crear una nueva vista e inflarla con el inflater desde un XML (este proceso puede ser algo lento). Pero para las siguientes llamadas, este parámetro contendrá la vista devuelta por nosotros en la llamada anterior, para esta posición. De esta forma ya no será necesario crearla desde cero y solo tendremos que modificar sus características y devolverla. El resto del método se utiliza para actualizar cada campo, según el lugar a representar.

NOTA: En este ejemplo, el BaseAdapter devuelve siempre el mismo tipo de vista. Aunque esta es la forma más habitual de trabajar, no tiene por qué ser necesariamente así. Cada elemento devuelto desde getView() podría ser una vista totalmente diferente de las demás.

Finalmente, tenemos que definir tres métodos que permiten acceder a la información representada. El método getCount() será el que indique cuántos elementos queremos mostrar. El método getItem() devolverá el objeto libro que se muestra en una determinada posición. El método getItemId() devolverá el id que se muestra en una determinada posición. El id es un valor numérico que identifica cada elemento. Si utilizamos un vector, se suele utilizar el índice del vector como id, mientras que en bases de datos suele ser el campo de indexación principal. En esta primera versión del adaptador se muestran todos los lugares en el mismo orden en que los tenemos en el vector; por lo tanto, el id coincide con la posición. Más adelante mostraremos un ejemplo más complejo donde ya no ocurrirá esto.

5.     Ejecuta la aplicacióny verifica el resultado.

NOTA: En algunos casos, el adaptador ha de trabajar con listas muy grandes o estas listas han de crearse desde un servidor. En estos casos es mejor ir solicitando la información a medida que se va representando. Se muestra un ejemplo en la aplicación ApiDemos descrita en el capítulo 1, en la actividad: com.example.android.apis.view.List13

Detectar una pulsación sobre un elemento de la lista

Un ListView puede tener diferentes componentes que nos permitan interaccionar con el usuario. Por ejemplo, cada elemento definido en getView() puede tener botones para diferentes acciones.

Hay un tipo de interacción muy sencilla de definir. La clase ListActivity tiene un método que es invocado cada vez que se pulsa sobre un elemento de la lista. El siguiente ejercicio ilustra cómo utilizarlo.

Ejercicio paso a paso: Detectar una pulsación sobre un elemento de la lista en Asteroides

1. Añade el siguiente método a la clase Puntuaciones.java:

@Override protected void onListItemClick(ListView listView, 
                         View view, int position, long id) {
   super.onListItemClick(listView, view, position, id);
   Object o = getListAdapter().getItem(position);
   Toast.makeText(this, "Selección: " + Integer.toString(position)
          +  " - " + o.toString(),Toast.LENGTH_LONG).show();
}

2. Ejecuta la aplicación, pulsa en “Puntuaciones” y luego en una puntuaciób para verificarel resultado.

[1] Para saber más sobre clases genéricas puedes ver el siguiente Polimedia http://youtu.be/N3yy2pfUaE0.

Ejercicio paso a paso: Detectar una pulsación sobre un elemento de la lista en Mis Lugares

1. Añade el siguiente método a la clase MainActivity de Mis Lugares:

@Override protected void onListItemClick(ListView listView, View vista, int
position, long id) {
   super.onListItemClick(listView, vista, position, id);
   Intent i = new Intent(this, VistaLugar.class);
   i.putExtra("id",id);
   startActivity(i);
}

El método onListItemClick() dispone de tres parámetros: el listViewque se ha pulsado, la vista pulsada dentro de este listViewy el iddel elemento pulsado.

2. Ejecuta la aplicación, pulsa sobre un lugar para ver su información detallada.

Preguntas de repaso y reflexión:  ListView