Tutorial De Listas Y Adaptadores En Android

Las listas en Android son contenedores supremamente útiles para organizar información en forma vertical y con la capacidad de usar scrolling (desplazamiento) para simplificar su visualización.

Esta técnica es muy popular en muchas aplicaciones, ya que permite mostrarle al usuario un conjunto de datos de forma práctica y accesible.

Google liberó un nuevo componente alternativo llamado RecyclerView para crear listas y grillas. Si deseas ver cómo usarlo ve al siguiente artículo: Crear una lista con RecyclerView

La Clase ListView En Android

La clase que representa una lista vertical en el API de Android se llama ListView. Esta clase viene preparada para recibir los ítems que desplegará en la interfaz, facilitando al programador la implementación de sus características y comportamientos.

Listas con la clase ListView en Android

Si en algún momento los ítems que contiene dificultan la visualización total en la actividad de la aplicación, automáticamente implementará scrolling para que el usuario pueda desplegar los elementos ocultos.

Estructuralmente un ListView contiene un View específico por cada fila. También se compone de un ScrollView, el cual permite generar el desplazamiento vertical por si se agota la pantalla para nuestros elementos.

CRM Leads App

Para darle ruta a los conocimientos que veremos a lo largo de este artículo, vamos a planificar una miniaplicación que resuelva alguna necesidad en especial, donde necesitemos crear una lista.

Se me ha ocurrido crear una app que muestre los Leads de un sistema online CRM (Custommer Relationship Management).

Esto suponiendo que los usuarios área de Marketing de alguna empresa hipotética, necesitan manejar la generación de leads para optimizar el flujo de ventas.

Wireframing de la característica

La idea es implementar una lista que muestre los siguientes datos de un lead:

  • Nombre
  • Cargo
  • Compañía a la que pertenece
  • Foto

Con ello en mente el siguiente es un bosquejo rápido con la ubicación de cada atributo:

Wireframing de app CRM Leads Android

Como ves, tendremos la foto (avatar) del lead en la parte izquierda centrada en el ítem. En la parte superior tendremos el nombre como texto primario. Y por debajo el cargo y la compañía como textos secundarios.

Descargar Proyecto Completo En Android Studio

Puedes descargar el proyecto final de CMR Leads en la siguiente caja de descargas:

Retomando, ahora veamos la programación…

Implementar Lista De Leads

Antes de iniciar, crea un nuevo proyecto en Android Studio llamado “CRM Leads”.

Crear actividad de leads

Añade una actividad del tipo Empty Activity y nombrala LeadsActivity.

El propósito de esta es actuar como el punto de entrada de la característica que vamos a crear.

Sobre ella añadiremos un fragmento, el cuál actuará como la implementación de la vista.

LeadsActivity.java

/**
 * Screen para el manejo de los leads en la app CRM
 */
public class LeadsActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leads);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // Agregar fragmento

    }


}

Crear fragmento para mostrar lista de leads

Haz click derecho en el paquete java de tu proyecto, selecciona New > Fragment > Fragment (Blank) y confirma la creación de un fragmento con el nombre LeadsFragment.

LeadsFragment.java

/**
 * Vista para los leads del CRM
 */
public class LeadsFragment extends Fragment {
    
    public LeadsFragment() {
        // Required empty public constructor
    }

    public static LeadsFragment newInstance(/*parámetros*/) {
        LeadsFragment fragment = new LeadsFragment();
        // Setup parámetros
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            // Gets parámetros
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_leads, container, false);
       
        return root;
    }
}

Añadir ListView al layout

En la definición XML las listas se agregan con el elemento <ListView>. Debido a que esta clase extiende de ViewGroup, es posible que lo uses como nodo raíz de un layout.

Su definición se vería similar a esta:

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

 

Si quieres cambiar aspectos en su contenido o comportamiento puedes usar atributos XML como los siguientes:

Atributo Función
android:divider Drawable o color para representar el divisor entre ítems. Por defecto verás una línea horizontal de color gris claro. Usa el valor @null si no quieres que aparezca.
android:dividerHeight Altura del divisor
android:entries Aquí puedes poner la referencia de un array de strings de tus recursos para poblar automáticamente la lista sin usar adaptadores.
android:footerDividersEnabled Habilita o deshabilita el divisor que va antes de un elemento especial llamado “footer”, el cuál va al final de la lista.
android:headerDividersEnabled Similar a android:footerDividersEnabled, solo que esta vez se refiere al divisor de un elemento especial llamado “header” que va al inicio de la lista.

Teniendo esto en cuenta, abre el layout del fragmento fragment_leads.xml y situa un ListView dentro al interior de un FrameLayout.

fragment_leads.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="8dp"
    tools:context="com.herprogramacion.crmleads.LeadsFragment">

    <ListView
        android:id="@+id/leads_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@null" />

</FrameLayout>

Si abres la ventana Preview en la tab Text del editor de layouts verás lo siguiente:

Preview de lista de Leads en Android Studio

Agregar fragmento a la actividad

Abre LeadsActivity y sitúate en onCreate(). Llama al administrador de fragmentos y realiza una transacción add() con una instancia de LeadsFragment. Recuerda asignar un id al elemento XML de la actividad que actuará como container.

// ...
LeadsFragment leadsFragment = (LeadsFragment)
        getSupportFragmentManager().findFragmentById(R.id.leads_container);

if (leadsFragment == null) {
    leadsFragment = LeadsFragment.newInstance();
    getSupportFragmentManager().beginTransaction()
            .add(R.id.leads_container, leadsFragment)
            .commit();
}

Si ejecutas la aplicación en este momento tendrías una pantalla en blanco.

¿Por qué?

La respuesta a esa inquietud está en la siguiente sección…

Poblar Lista Con Un Adaptador

Un adaptador es un objeto que comunica a un ListView los datos necesarios para crear las filas de la lista. Es decir, conecta la lista con una fuente de información como si se tratase de un adaptador de corriente que alimenta a un televisor.

Además de proveer la información, también genera los Views para cada elemento de la lista.

Los adaptadores se representan programáticamente por la clase BaseAdapter. Dependiendo de la naturaleza de la lista se elegirá un adaptador prefabricado en el SDK de Android o extenderlos para satisfacer tus necesidades.

Interacción ListView-Adapter

Cuando relacionas un adaptador a una lista, inmediatamente comienza un proceso de comunicación interno para poblarla con una fuente de datos.

Dicha comunicación se basa principalmente en los siguientes métodos del adaptador:

  • getCount(): Retorna en la cantidad de elementos de la fuente de datos. Con este valor la lista ya puede establecer un límite para añadir items.
  • getItem(): Obtiene un elemento de la fuente de datos asignada al adaptador en una posición establecida. Normalmente la fuente de datos es una lista de objetos o un Cursor.
  • getView(): Retorna en el View inflado y ligado a los datos según su posición.

Aunque estos tres métodos no son los únicos que existen para establecer la relación, a mi parecer son los más significativos para entender el concepto de un adaptador.

El flujo de interacciones sería el siguiente:

L: — ¿Cuantos elementos hay para hoy?
A: — Existen “getCount()” elementos en la fuente
L: — ¡Comprendo!… muestrame el elemento 3
A: — Si claro, el contenido es “getItem(2)”
L: — Mmm…ya veo… ¿Y cuál sería su View?
A: — Al elemento 3 le corresponde “getView(2)”
L: — Ok, lo mostraré al usuario

Si aún no te ha quedado claro, entonces puedes ver esta ilustración sobre cómo sería esta relación:
Comunicación entre una lista y un adaptador

La clase ArrayAdapter

Como habíamos dicho, Android nos provee subclases de adaptadores que nos facilitan la implementación. ArrayAdapter es uno de los tipos de adaptadores más sencillos y populares.

Debido a que es una clase genérica, puedes usar cualquier objeto para poblar la lista.

Este adaptador infla los ítems con un layout preestablecido de Android, invoca el método toString() de cada elemento y lo setea automáticamente en uno o dos TextViews según el diseño.

ListView con elementos simples

Implementar ArrayAdapter de leads

Para implementar un ArrayAdapter en una lista simplemente seguimos los siguientes pasos:

1. Abre LeadsFragment y declara un atributo para la lista y otro para un ArrayAdapter con tipo String:

ListView mLeadsList;
ArrayAdapter<String> mLeadsAdapter;
//...

2. Obtén programáticamente una referencia de la lista en onCreateView():

// Instancia del ListView.
mLeadsList = (ListView) root.findViewById(R.id.leads_list);

3. Allí mismo crea un array de Strings con nombres de ejemplo e inicializa el adaptador con el constructor ArrayAdapter<T>(Context, int, T[]):

// ...
String[] leadsNames = {
        "Alexander Pierrot",
        "Carlos Lopez",
        "Sara Bonz",
        "Liliana Clarence",
        "Benito Peralta",
        "Juan Jaramillo",
        "Christian Steps",
        "Alexa Giraldo",
        "Linda Murillo",
        "Lizeth Astrada"
};

mLeadsAdapter = new ArrayAdapter<>(
        getActivity(),
        android.R.layout.simple_list_item_1,
        leadsNames);

Donde los parámetros del constructor tienen el siguiente propósito:

  • Context context: Representa el contexto de la aplicación. Usamos getActivity() para indicar que será la actividad.
  • int resource: Es el recurso de diseño o layout que representará cada fila de la lista. En este caso usamos un recurso del sistema llamado simple_list_item_1.xml. Este layout contiene un solo TextView que contendrá el texto de cada fila. Si deseas dos líneas de texto, usa simple_list_item_2.xml.
  • T[] objects: Array genérico con los datos a inflar en la lista. También puedes usar una lista List<T>.

4. Vincula la lista con el adaptador a través del método ListView.setAdapter():

mLeadsList.setAdapter(mLeadsAdapter);

Esta referencia inicia el proceso de llenado de la lista.

Por lo que al ejecutar la app CRM en este punto tendrás:

CRM Leads

Este no es el diseño propuesto al inicio, sin embargo es un buen ejemplo de ArrayAdapter con un diseño prefabricado.

Crear tu propia fuente de datos

Si la información que vas a usar en los elementos de la lista tiene formatos especiales o un manejo complejo, es mejor crear una nueva clase que represente a cada ítem.

Esto es sencillo, solo crea una nueva clase llamada Lead y agrega los atributos que vimos al inicio del problema junto a un ID.

Luego autogenera el constructor, gets, sets y el método toString():

Lead.java

/**
 * Entidad Lead
 */
public class Lead {
    private String mId;
    private String mName;
    private String mTitle;
    private String mCompany;
    private int mImage;

    public Lead(String name, String title, String company, int image) {
        mId = UUID.randomUUID().toString();
        mName = name;
        mTitle = title;
        mCompany = company;
        mImage = image;
    }

    public String getId() {
        return mId;
    }

    public void setId(String mId) {
        this.mId = mId;
    }

    public String getName() {
        return mName;
    }

    public void setName(String mName) {
        this.mName = mName;
    }

    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String title) {
        this.mTitle = title;
    }

    public String getCompany() {
        return mCompany;
    }

    public void setCompany(String mCompany) {
        this.mCompany = mCompany;
    }

    public int getImage() {
        return mImage;
    }

    public void setImage(int mImage) {
        this.mImage = mImage;
    }

    @Override
    public String toString() {
        return "Lead{" +
                "ID='" + mId + '\'' +
                ", Compañía='" + mCompany + '\'' +
                ", Nombre='" + mName + '\'' +
                ", Cargo='" + mTitle + '\'' +
                '}';
    }
}

Crear layout personalizado para ítem de la lista

Crea un nuevo layout llamado list_item_lead.xml.

Establece un RelativeLayout como nodo e intenta generar las ubicaciones de cada dato del lead como se muestra en el siguiente mock de alto nivel:

Ítem de lista personalizado con imagen y texto

El siguiente sería el resultado final:

list_item_lead.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?attr/listPreferredItemHeight"
    android:orientation="vertical"
    android:paddingBottom="8dp"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="8dp">

    <ImageView
        android:id="@+id/iv_avatar"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_centerVertical="true" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="56dp"
        android:text="Nombre"
        android:textAppearance="?attr/textAppearanceListItem"
        tools:text="Alexander Pierrot" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/tv_name"
        android:layout_below="@+id/tv_name"
        android:text="Cargo"
        tools:text="CEO" />

    <TextView
        android:id="@+id/tv_company"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/tv_name"
        android:layout_below="@+id/tv_title"
        android:text="Compañía"
        tools:text="Blue Insurances Ltd" />
</RelativeLayout>

Donde los siguientes son aspectos a resaltar:

  • Usa el atributo prestablecido ?attr/listPreferredItemHeight para evitar que la altura mínima del ítem sea menor a las normas de diseño en Android.
  • El nombre del lead es el texto primario del ítem. Evita acomodar manualmente su estilo con la apariencia predefinida ?attr/textAppearanceListItem.
  • El atributo tools:text permite renderizar texto experimental en la ventana Preview, sin tener que ejecutar la app para ver el resultado.

Crear adaptador personalizado

1. El adaptador que tienes hasta ahora no funciona para cambiar el contenido de la imagen y los tres textos. Esto te obliga a sobrescribir ArrayAdapter.

Para ello crea una nueva clase Java llamada LeadsAdapter y extiéndela del tipo ArrayAdapter<Lead>.

public class LeadsAdapter extends ArrayAdapter<Lead> {

2. Luego crea un constructor que reciba el contexto y la lista leads. Llama al constructor padre que usamos anteriormente con super() y agrega los dos parámetros entrantes. Debido a que el segundo parámetro es el layout pasa 0, ya que usaremos un propio.

3. El método getView() es el que infla los ítems y liga los datos de los objetos Lead, así que el objetivo es sobrescribir este método.

Los parámetros que recibe son:

  • int position: La posición actual del elemento a procesar
  • View convertView: El view actual a procesar. Puede ser null si no se ha inflado aún.
  • ViewGroup parent: El padre al cual el ítem será insertado, en este caso será el ListView.

Con esto en mente, la receta a seguir sería:

  1. Obtener referencia del componente LayoutInflater.
  2. Inflar el layout del ítem si el view no existe
  3. Obtén las instancias de los views a modificar
  4. Obtén la instancia actual de la fuente de datos, extrae sus valores y haz el setup en los views.

Veamos:

/**
 * Adaptador de leads
 */
public class LeadsAdapter extends ArrayAdapter<Lead> {
    public LeadsAdapter(Context context, List<Lead> objects) {
        super(context, 0, objects);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // Obtener inflater.
        LayoutInflater inflater = (LayoutInflater) getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // ¿Existe el view actual?
        if (null == convertView) {
            convertView = inflater.inflate(
                    R.layout.list_item_lead,
                    parent,
                    false);
        }

        // Referencias UI.
        ImageView avatar = (ImageView) convertView.findViewById(R.id.iv_avatar);
        TextView name = (TextView) convertView.findViewById(R.id.tv_name);
        TextView title = (TextView) convertView.findViewById(R.id.tv_title);
        TextView company = (TextView) convertView.findViewById(R.id.tv_company);

        // Lead actual.
        Lead lead = getItem(position);

        // Setup.
        Glide.with(getContext()).load(lead.getImage()).into(avatar);
        name.setText(lead.getName());
        title.setText(lead.getTitle());
        company.setText(lead.getCompany());
       
        return convertView;
    }
}

La librería Glide permite cargar imágenes de forma asíncrona para no entorpecer el hilo principal de la UI. Para usarla agrega la siguiente dependencia a build.gradle a nivel de módulo:

compile 'com.github.bumptech.glide:glide:3.7.0'

 

Crear repositorio de datos

Lo último que falta es generar los leads que añadiremos a nuestro a adaptador.

Crea una nueva clase Java con patrón Singleton que represente un origen de datos ficticio del cual se alimentará el adaptador. Usa el nombre LeadsRepository.

LeadsRepository.java

/**
 * Repositorio ficticio de leads
 */
public class LeadsRepository {
    private static LeadsRepository repository = new LeadsRepository();
    private HashMap<String, Lead> leads = new HashMap<>();

    public static LeadsRepository getInstance() {
        return repository;
    }

    private LeadsRepository() {
        saveLead(new Lead("Alexander Pierrot", "CEO", "Insures S.O.", R.drawable.lead_photo_1));
        saveLead(new Lead("Carlos Lopez", "Asistente", "Hospital Blue", R.drawable.lead_photo_2));
        saveLead(new Lead("Sara Bonz", "Directora de Marketing", "Electrical Parts ltd", R.drawable.lead_photo_3));
        saveLead(new Lead("Liliana Clarence", "Diseñadora de Producto", "Creativa App", R.drawable.lead_photo_4));
        saveLead(new Lead("Benito Peralta", "Supervisor de Ventas", "Neumáticos Press", R.drawable.lead_photo_5));
        saveLead(new Lead("Juan Jaramillo", "CEO", "Banco Nacional", R.drawable.lead_photo_6));
        saveLead(new Lead("Christian Steps", "CTO", "Cooperativa Verde", R.drawable.lead_photo_7));
        saveLead(new Lead("Alexa Giraldo", "Lead Programmer", "Frutisofy", R.drawable.lead_photo_8));
        saveLead(new Lead("Linda Murillo", "Directora de Marketing", "Seguros Boliver", R.drawable.lead_photo_9));
        saveLead(new Lead("Lizeth Astrada", "CEO", "Concesionario Motolox", R.drawable.lead_photo_10));
    }

    private void saveLead(Lead lead) {
        leads.put(lead.getId(), lead);
    }

    public List<Lead> getLeads() {
        return new ArrayList<>(leads.values());
    }
}

Tendremos 10 ejemplos para probar la lista. También habrá un método para guardar nuevas instancias saveLead() y otro para retornar en todos los objetos del repositorio getLeads().

Cabe aclarar que los avatares estarán en los recursos drawables, por lo que guardamos su identificador entero como referencia.

Integrar todo en la aplicación

Ahora solo queda volver a LeadsFragment, crear una instancia del nuevo adaptador y añadir los datos del repositorio.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.fragment_leads, container, false);

    // Instancia del ListView.
    mLeadsList = (ListView) root.findViewById(R.id.leads_list);

    // Inicializar el adaptador con la fuente de datos.
    mLeadsAdapter = new LeadsAdapter(getActivity(),
            LeadsRepository.getInstance().getLeads());

    //Relacionando la lista con el adaptador
    mLeadsList.setAdapter(mLeadsAdapter);

    return root;
}

Ejecuta y verás el siguiente resultado:

Sistema CRM para manejo de Leads

 

Estamos probando un ejemplo estático. En casos reales es posible que los datos vengan de una base de datos SQLite, un Web Service u otra fuente de datos concurrente.

Actualizar un ListView

Si en algún momento los datos de la lista cambian, el encargado de actualizar los elementos será el adaptador. El método que informa dichas modificaciones es notifyDataSetChanged(). Cuando se llama, los views son refrescados y así el usuario percibe el cambio en tiempo real.

Este método es ejecutado automáticamente por el adaptador cuando se llaman métodos que modifiquen la fuente de datos.

Algunos son:

  • add(): Añade un nuevo elemento al final de la lista.
  • insert(): Añade un nuevo elemento en una posición especificada de la lista.
  • remove(): Elimina un elemento de la lista
  • clear(): Elimina todos los elementos de la lista.

Por ejemplo…

Podemos añadir un Action Button a la Action Bar llamado “ELIMINAR TODO”, el cual tendrá como fin limpiar todos los elementos de la lista a través del método clear() del adaptador.

Esto requiere que:

1. Habilites la contribución del fragmento a la action bar con el método setHasOptionsMenu en onCreateView():

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {

    // ...

    setHasOptionsMenu(true);
    return root;
}

2. Crees un nuevo recurso de menú llamado menu_leads_list.xml con el ítem de eliminación:

menu_leads_list.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_delete_all"
        android:title="@string/action_delete_all"
        app:showAsAction="ifRoom" />
</menu>

3. Infles el menú en el fragmento con onCreateOptionsMenu():

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.menu_leads_list, menu);
}

4. Proceses el evento del click con onOptionsItemSelected():

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    if (id == R.id.action_delete_all) {
        // Eliminar todos los leads
        mLeadsAdapter.clear();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

Al probar veremos que automáticamente los elementos de la lista han sido erradicados:

Clear ListView Android

Manejar Eventos De Click En Items

Si deseas ejecutar acciones cuando el usuario selecciona algún ítem de la lista, entonces debes setear una escucha del tipo OnItemClickListener.

Esta interfaz permite disparar el método onItemClick(), el cual nos permitirá ejecutar las acciones que deseamos automatizar.

Por ejemplo…

Abre LeadsFragment y dentro de onCreateView() setea una escucha anónima con setOnItemClickListener(). En el controlador despliega un Toast para mostrar el nombre del ítem clickeado.

// Eventos
mLeadsList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
        Lead currentLead = mLeadsAdapter.getItem(position);
        Toast.makeText(getActivity(),
                "Iniciar screen de detalle para: \n" + currentLead.getName(),
                Toast.LENGTH_SHORT).show();
    }
});

onItemClick() recibe tres parámetros. El primero es el View que usa al adaptador, en este caso la lista. El segundo es el View del ítem que ha sido presionado, el tercero hace referencia a la posición del ítem en la fuente de datos que maneja el adaptador y el cuarto es un identificador del elemento.

Simplemente obtuvimos el ítem Lead presionado en el adaptador con la posición de entrada. Luego de ello construimos una cadena que indique el nombre. Y finalmente desplegamos en pantalla un Toast con los datos:

Procesar eventos de click en lista

Un Toast es una pequeña ventana emergente con una duración determinada, el cual despliega un mensaje para notificar al usuario el cumplimiento de alguna acción. Su método makeText() genera una instancia prefabricada para simplificar la creación.

Personalizar el Selector de una Lista

¿Qué es un StateListDrawable?

Los fondos que se proyectan en un View al interactuar con la interfaz de usuario, son representados por un objeto drawable llamado StateListDrawable. Este está compuesto por una serie de drawables relacionados a un estado de interacción del componente.

¿Has visto que los ítems de un Listview toman un color azul al ser presionados?, cuando está iluminado se le llama estado de Presionado (pressed).

Ese fondo que toma momentáneamente el ítem es generado a través de un StateListDrawable predeterminado por los temas y estilos que estemos utilizando. Existen otros estados como “Presionado sostenidamente“(long pressed), Enfocado (focusabled), Seleccionado (selected), etc.

Crear un Selector para un ListView

Para declarar previamente el StateListDrawable de un view antes de inflarlo, podemos usar un archivo de recursos de tipo drawable. Donde el nodo que relacionará las imágenes con la lista de estados es representado por el elemento <selector>. Dentro de él declararemos elementos <item> que contendrán la información sobre cada drawable.

Veamos un ejemplo:

item_list_indicator.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:drawable="@drawable/list_pressed" /><!-- Pressed -->
</selector>

La anterior descripción asigna un drawable al estado stated_pressed (presionado).

Para habilitar la transición de fondo, el estado debe tener el valor de true.

En el atributo drawable puedes usar una referencia a tus recursos. La práctica común es crear NinePatches para la flexibilidad del fondo en cualquier tipo de dimensiones y densidades (si descargas el código verás los que se usaron).

Una vez creada tu lista de estados, pasamos a asignarla al ListView. Para ello usamos el atributo listSelector, el cual representa la lista de estados.

<ListView
    android:id="@+id/leads_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@null"
    android:listSelector="@drawable/item_list_indicator"/>

Si ejecutas y presionas un ítem verás:

List selector personalizado en ListView

Ripple Effect En ListView

Por otro lado, puedes asignar un efecto ripple al presionar los elementos.

Ripple Effect en ListView Android

Recuerda que esta es una tendencia de Material Design para transmitir al usuario que los ítems actúan como superficies que reaccionan físicamente ante el contacto.

Para hacerlo puedes crear tu propio elemento ripple:

<code><span class="com"> <ripple android:color="?android:attr/colorControlHighlight">
   <item android:id="@android:id/mask"
         android:drawable="@android:color/white" />
 </ripple></span></code>

O usar en el background del nodo principal del ítem de lista el atributo ?attr/selectableItemBackground.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:background="?attr/selectableItemBackground"">

Patrón ViewHolder Para Optimizar La Lista

Un adaptador personalizado llama un gran cantidad de veces el método findViewById() para obtener las referencias de los views del ítem.

Para reducir este trabajo considerablemente, Google recomienda usar el patrón ViewHolder. Una clase que almacena las referencias de los views hijos del ítem, con el objetivo de acceder a ellas en el futuro.

Este consiste en:

  • Crear una clase anidada dentro del adapter con referencias a los views
  • Comprobar en getView() si las referencias ya fueron guardadas
    • Si no es así, entonces se crea un nuevo ViewHolder para salvarlas
    • De lo contrario, se usan las referencias existentes

Con este flujo, LeadsAdapter quedaría así:

LeadsAdapter.java

/**
 * Adaptador de leads
 */
public class LeadsAdapter extends ArrayAdapter<Lead> {
    public LeadsAdapter(Context context, List<Lead> objects) {
        super(context, 0, objects);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // Obtener inflater.
        LayoutInflater inflater = (LayoutInflater) getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        ViewHolder holder;

        // ¿Ya se infló este view?
        if (null == convertView) {
            //Si no existe, entonces inflarlo con image_list_view.xml
            convertView = inflater.inflate(
                    R.layout.list_item_lead,
                    parent,
                    false);

            holder = new ViewHolder();
            holder.avatar = (ImageView) convertView.findViewById(R.id.iv_avatar);
            holder.name = (TextView) convertView.findViewById(R.id.tv_name);
            holder.title = (TextView) convertView.findViewById(R.id.tv_title);
            holder.company = (TextView) convertView.findViewById(R.id.tv_company);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        // Lead actual.
        Lead lead = getItem(position);

        // Setup.
        holder.name.setText(lead.getName());
        holder.title.setText(lead.getTitle());
        holder.company.setText(lead.getCompany());
        Glide.with(getContext()).load(lead.getImage()).into(holder.avatar);

        return convertView;
    }

    static class ViewHolder {
        ImageView avatar;
        TextView name;
        TextView title;
        TextView company;
    }
}