Ejemplo De SwipeRefreshLayout Mas RecyclerView En Android

¿Quieres cargar más elementos en tu lista usando un indicador de actividad circular que muestran muchas otras aplicaciones Android?

Entonces este tutorial es para ti, ya que te enseñará a usar el patrón de diseño “Swipe to Refresh“, el cual refresca el contenido de una view con tan solo arrastrar verticalmente
la interfaz.

Para comprender este tema crearemos una pequeña aplicación llamada Revista Tempor. Esta aplicación se basa en poblar una lista con artículos que serán votados por los usuarios.

Veamos un poco su funcionamiento:


¿Te gustaría obtener el proyecto en Android Studio completico?, entonces sigue estas instrucciones:

Asegúrate de tener claros los siguientes temas para comenzar con pie derecho el tutorial:

1. Crear un nuevo proyecto en Android Studio

El primer paso es crear un nuevo proyecto con una actividad en blanco. Esta acción se lleva a cabo yendo al menú File y luego seleccionando la opción New Project…

2. Diseñar Actividad Principal de la Aplicación

Debido a que la actividad principal está basada en una lista, es posible utilizar el elemento <android.support.v7.widget.RecyclerView> como nodo raíz. Con ello se tendríamos un layout como el siguiente:

<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/reciclador"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="3dp"
    android:scrollbars="vertical" />

3. Diseñar ítems para el RecyclerView

El siguiente paso es crear el layout para los ítems de la lista. Como se vio en el gif de la parte inicial, cada ítem se compone del título de la lista, la cantidad de votos que lleva hasta el momento y una imagen lateral que hace alusión al contenido del ítem. Este diseño lo hemos realizado un sin número de veces por lo que no requiere explicación.

Veamos:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:padding="10dp">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/imagen"
        android:src="@drawable/portafolio"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="false"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Medium Text"
        android:id="@+id/titulo"
        android:layout_toRightOf="@+id/imagen"
        android:layout_marginBottom="3dp"
        android:layout_alignParentTop="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Small Text"
        android:id="@+id/votos"
        android:textColor="#ff7e7e7e"
        android:layout_alignLeft="@+id/titulo"
        android:layout_alignParentBottom="true" />

</RelativeLayout>

4. Crear una fuente de fatos para poblar la lista

Hasta ahora ya sabemos que los ítems de la lista tienen una estructura definida. Estos poseen tres atributos fundamentales para ser representados lógicamente en una clase. Así que definiremos una nueva entidad llamada Lista, la cual tiene el siguiente esquema:

public class Lista {
    private String titulo;
    private String votos;
    private int idImagen;

    public Lista(String titulo, String votos, int idImagen) {
        this.titulo = titulo;
        this.votos = votos;
        this.idImagen = idImagen;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getVotos() {
        return votos;
    }

    public void setVotos(String votos) {
        this.votos = votos;
    }

    public int getIdImagen() {
        return idImagen;
    }

    public void setIdImagen(int idImagen) {
        this.idImagen = idImagen;
    }
}

Si nos detenemos a pensar por un instante en la lógica de la carga regular de nuevos ítems, surgirá la siguiente pregunta:

¿De dónde se obtendrán una gran variedad de elementos, si no vamos a conectarnos en tiempo real a un servidor externo? La respuesta está en la duplicación aleatoria.

Así es, crearemos un array de elementos de tipo Lista donde guardaremos una cantidad fija de elementos con sus respectivas características. Luego se creará un método que retorne en una lista que contenga ítems seleccionados al azar.

Esto permitirá mostrar variación a la hora de refrescar los ítems. Con esta forma de trabajo al menos nuestro ejemplo no quedará extremadamente estático.

La traducción de todo lo dicho es la creación de la clase ConjuntoLista, la cual contiene el arreglo de elementos predeterminados y el método de distribución aleatoria:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;

/**
 * Creado por Hermosa Programación
 */
public class ConjuntoListas {

    static final Lista LISTAS[] = {
            new Lista("Lista de las 10 mejores herramientas de analiticas", "3022", R.drawable.analytics),
            new Lista("Lista de los mejores servicios de entrega en Cali", "1234", R.drawable.box),
            new Lista("Lista de los navegadores mas rápidos", "2452", R.drawable.browser),
            new Lista("Lista de las aplicaciones mas populares de chat", "4532", R.drawable.bubbles),
            new Lista("Lista de los hábitos que te hacen perder tiempo", "24321", R.drawable.clock),
            new Lista("Lista de las joyas mas deseadas por las mujeres", "9090", R.drawable.diamond),
            new Lista("Lista de los países mas ricos del mundo", "256", R.drawable.graph),
            new Lista("Lista de las computadoras MAC mas caras", "2453", R.drawable.imac),
            new Lista("Lista de los videojuegos mas jugados", "1112", R.drawable.joypad),
            new Lista("Lista de los mejores DJs de Europa", "4512", R.drawable.keyboards),
            new Lista("Lista de los actores mas guapos de Bollywood", "123", R.drawable.man),
            new Lista("Lista de los los lugares turisticos mas hermosos", "4452", R.drawable.map),
            new Lista("Lista de los servicios en la nube con mayor espacio gratis", "2222", R.drawable.open_box),
            new Lista("Lista de los peores regalos que puedas dar", "3849", R.drawable.pack),
            new Lista("Lista de 20 oportunidades de negocio para jovenes", "456", R.drawable.portafolio),
            new Lista("Lista de las 10 herramientas de edición musical mas usadas", "7840", R.drawable.settings),
            new Lista("Lista de los conciertos mas esperados de octubre", "9808", R.drawable.speakers),
            new Lista("Lista de objetivos mas comunes de un Desarrollador Android", "1234", R.drawable.target),
            new Lista("Lista de los 5 peores licores de China", "5556", R.drawable.wine),
            new Lista("Lista de las 10 mujeres mas deseadas del equipo Google", "567", R.drawable.woman)
    };

    /**
     * Este método retorna en una lista aleatoria basada en el
     * atributo LISTAS.
     *
     * El parámetro entero count es el tamaño deseado de la lista
     * resultante
     */
    public static ArrayList randomList(int count) {
        Random random = new Random();
        HashSet items = new HashSet();

        // Restricción de tamaño
        count = Math.min(count, LISTAS.length);

        while (items.size() < count) {
            items.add(LISTAS[random.nextInt(LISTAS.length)]);
        }

        return new ArrayList(items);
    }
}

Como ves, me he tomado el trabajo de crear 20 elementos distintos para generar variedad. Adicionalmente encontramos el método randomList(), donde se rellena un conjunto de datos hash que apuntan a un índice aleatorio del arreglo LISTAS.

5. Crear un Adaptador para la lista

Teniendo en cuenta que la fuente de datos contiene solo tres atributos, entonces debemos proceder a inflar los views de cada ítem en una instancia RecyclerView.Adapter.

package com.herprogramacion.revistatempor;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

/**
 * Creado por Hermosa Programación
 */
public class ListaAdapter extends RecyclerView.Adapter<ListaAdapter.RevistaViewHolder> {

    private List<Lista> items;

    public static class RevistaViewHolder extends RecyclerView.ViewHolder {
        // Campos de la lista
        public ImageView imagen;
        public TextView titulo;
        public TextView votos;

        public RevistaViewHolder(View v) {
            super(v);
            imagen = (ImageView) v.findViewById(R.id.imagen);
            titulo = (TextView) v.findViewById(R.id.titulo);
            votos = (TextView) v.findViewById(R.id.votos);
        }
    }

    public ListaAdapter(List<Lista> items) {
        this.items = items;
    }

    /*
    Añade una lista completa de items
     */
    public void addAll(List<Lista> lista){
        items.addAll(lista);
        notifyDataSetChanged();
    }

    /*
    Permite limpiar todos los elementos del recycler
     */
    public void clear(){
        items.clear();
        notifyDataSetChanged();
    }

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

    @Override
    public RevistaViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.lista, viewGroup, false);
        return new RevistaViewHolder(v);
    }

    @Override
    public void onBindViewHolder(RevistaViewHolder viewHolder, int i) {
        viewHolder.imagen.setImageResource(items.get(i).getIdImagen());
        viewHolder.titulo.setText(items.get(i).getTitulo());
        viewHolder.votos.setText("Votos: "+String.valueOf(items.get(i).getVotos()));
    }
}

Es necesario destacar que tenemos dos nuevos métodos que no se habían implementado antes. Se trata de addAll() y clear(). El primero permite que añadamos un conjunto de elementos directamente hacia la fuente alimentadora de datos del adaptador, que en este caso es el atributo items.

Tanto addAll() como clear() usan el método notifyDataSetChanged(), el cual notifica al adaptador que la fuente de datos ha cambiado y que por ende es necesario refrescar la lista. Con esas implementaciones ya estamos listos para remover y añadir elementos al momento de realizar un Swipe para la recarga.

6. Implementar la clase SwipeRefreshLayout

La clase android.support.v4.widget.SwipeRefreshLayout es el motivo del porqué estás leyendo este artículo, ya que es la que permite implementar el refresco del contenido de un ítem a través de un gesto de arrastre vertical. La metódica consiste en envolver al view que deseamos refrescar (en este caso el recycler) dentro de un elemento SwipeRefreshLayout.

Cabe aclarar que este elemento solo puede contener un view en su jerarquía, porque se concentra en la superficie de un único elemento. Por eso debes usar match_parent para que el elemento hijo sea abarcado por completo y el indicador circular se presente efectivamente.

Esta situación modifica el panorama que teníamos hasta el momento. Así que se debe modificar el layout de la actividad principal para que el recycler view sea hijo de un elemento de un refresh layout.

<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipeRefresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/reciclador"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="3dp"
        android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>

7. Implementar la escucha OnRefreshListener

Una vez indicado el hijo del refresh layout, procederemos a manejar el evento de refresco. La idea es sobrescribir el método onRefresh() de la escucha OnRefreshListener, el cual es el encargado de dictaminar las acciones que se ejecutarán, cuando la señal se haya concretado en la interfaz.

Usa el método setOnRefreshListener() de la instancia SwipeRefreshLayout para asignar la escucha construida (bien sea anónimamente o implementando la interfaz sobre la actividad principal):

private SwipeRefreshLayout refreshLayout;
..

// Obtener el refreshLayout
refreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefresh);

// Iniciar la tarea asíncrona al revelar el indicador
refreshLayout.setOnRefreshListener(
    new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            new HackingBackgroundTask().execute();
        }
    }
);

Al ser ejecutado onRefresh() se está iniciando una tarea asíncrona que realiza la actualización del adaptador y además simula el retardo de la carga cuando se retarda el hilo con el método sleep().

private class HackingBackgroundTask extends AsyncTask<Void, Void, List<Lista>> {

        static final int DURACION = 3 * 1000; // 3 segundos de carga

        @Override
        protected List doInBackground(Void... params) {
            // Simulación de la carga de items
            try {
                Thread.sleep(DURACION);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // Retornar en nuevos elementos para el adaptador
            return ConjuntoListas.randomList(CANTIDAD_ITEMS);
        }

        @Override
        protected void onPostExecute(List result) {
            super.onPostExecute(result);

            // Limpiar elementos antiguos
            adapter.clear();

            // Añadir elementos nuevos
            adapter.addAll(result);

            // Parar la animación del indicador
            refreshLayout.setRefreshing(false);
        }

    }

La tarea asíncrona HackingAsynTask no recibe parámetros para su funcionamiento ni tampoco unidades de progreso. Pero debido a que vamos a presentar como resultado una lista de objetos Lista en el hilo principal, entonces usamos el tipo List<Lista> como tercer parámetro de entrada.

doInBackGround() procesa el retardo temporal para la simulación de la carga y al final se retorna en una lista aleatoria con el método randonList(). La constante CANTIDAD_ITEMS está definida al inicio de la actividad, la cual especifica que solo se desean 8 ítems por carga.

Luego se implementa onPostExecute() para refrescar los items. Lo primero es limpiar con clear(), luego añadir el resultado con addAll() y finalmente detener la animación del indicador con el método setRefreshing(). Este recibe un parámetro booleano, donde true inicia la animación y false la detiene.

8. Cambiar color de la animación de progreso

Adicionalmente podemos cambiar el color de la animación de progreso a través del método setColorSchemeResource(). Este método recibe una lista indeterminada de enteros que representan el id del recurso establecido en el archivo colors.xml.

// Seteamos los colores que se usarán a lo largo de la animación
refreshLayout.setColorSchemeResources(
        R.color.s1,
        R.color.s2,
        R.color.s3,
        R.color.s4
);

Dependiendo del tiempo de carga así mismo se dará espacio para la transición entre los colores usados. Por ejemplo, el código anterior usa 4 colores intermedios de la paleta para Teal en el Material Design. Cada uno de ellos se encuentra en el archivo de recursos para colores:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="s1">#A7FFEB</color>
    <color name="s2">#64FFDA</color>
    <color name="s3">#1DE9B6</color>
    <color name="s4">#00BFA5</color>
</resources>

Y ya para terminar unificamos todo dentro de la actividad principal, donde crearemos el adaptador, obtendremos el recycler view, implementaremos los eventos, etc:

package com.herprogramacion.revistatempor;

import android.os.AsyncTask;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.List;


public class MainActivity extends ActionBarActivity {

    /*
    Declarar instancias globales
    */
    private RecyclerView recycler;
    private ListaAdapter adapter;
    private RecyclerView.LayoutManager lManager;

    private SwipeRefreshLayout refreshLayout;

    private static final int CANTIDAD_ITEMS = 8;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Obtener el Recycler
        recycler = (RecyclerView) findViewById(R.id.reciclador);

        // Usar un administrador para LinearLayout
        lManager = new LinearLayoutManager(this);
        recycler.setLayoutManager(lManager);

        // Crear un nuevo adaptador
        adapter = new ListaAdapter(ConjuntoListas.randomList(CANTIDAD_ITEMS));
        recycler.setAdapter(adapter);

        // Obtener el refreshLayout
        refreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefresh);

        // Seteamos los colores que se usarán a lo largo de la animación
        refreshLayout.setColorSchemeResources(
                R.color.s1,
                R.color.s2,
                R.color.s3,
                R.color.s4
        );

        // Iniciar la tarea asíncrona al revelar el indicador
        refreshLayout.setOnRefreshListener(
            new SwipeRefreshLayout.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    new HackingBackgroundTask().execute();
                }
            }
        );

    }

    private class HackingBackgroundTask extends AsyncTask<Void, Void, List<Lista>> {

        static final int DURACION = 3 * 1000; // 3 segundos de carga

        @Override
        protected List doInBackground(Void... params) {
            // Simulación de la carga de items
            try {
                Thread.sleep(DURACION);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // Retornar en nuevos elementos para el adaptador
            return ConjuntoListas.randomList(CANTIDAD_ITEMS);
        }

        @Override
        protected void onPostExecute(List result) {
            super.onPostExecute(result);

            // Limpiar elementos antiguos
            adapter.clear();

            // Añadir elementos nuevos
            adapter.addAll(result);

            // Parar la animación del indicador
            refreshLayout.setRefreshing(false);
        }

    }
}

El resultado final se puede ver en la siguiente ilustración:

Aplicación Android con Patrón Swipe Refresh
Todos los créditos de los iconos para IconFinder.