Android RecyclerView: Listas verticales y horizontales

Última actualización:10/04/2016

android

El widget RecyclerView es una de las novedades más importante que ha recibido Android en los últimos tiempos desde el punto de vista del desarrollador ya que permite reemplazar widgets fundamentales como ListView y GridView por “algo mejor” y más potente.

RecyclerView es un ViewGroup cuya misión es mostrar Views que se repiten múltiples veces, por ejemplo un listado, de forma que estas Views se van reutilizando para generar solamente aquellas que se muestran en pantalla. Este comportamiento también lo tiene ListView, pero RecyclreView obliga a implementar el ViewHolderPattern para mejorar el rendimiento evitando llamadas a findById que pueden ralentizar enormemente el scroll. En la ListView, es responsabilidad del programador seguir el ViewHolderPattern a la hora de implementar un Adapter.

RecyclerView por sí mismo no proporciona mucho más que este reciclado y un listener para seguir el scroll. De hecho, ni siquiera posiciona los elementos en pantalla o gestiona eventos que no sean el scroll y delega en otras clases la realización de algunas funcionalidades (por ejemplo las animaciones) .

La simplicidad y modularidad convierten a RecyclerView en un poderoso componente porque permite construir cualquier interfaz gráfica en la que se requiera mostrar un listado de datos sin tener que limitarnos a lo ofrecido por ListView o GridView. Por ejemplo, con RecyclerView podemos tener “ListView” horizontales (se verá al final de este tutorial), animar fácilmente los elementos de la vista, trabaja con múltiples layouts y tipos de divisores, integración con CoordinatorLayout…tareas no contempladas por ListView y que suelen ser muy complicadas de implementar o directamente inviables.

Mostrar un listado

Empecemos a utilizar ReciclerView creando un listado típico con scroll vertical, esto es , equivalente a un ListView.

  1. Importar el módulo de la librería de compatibilidad que proporciona RecyclerView y sus clases asociadas. En Android Studio/Gradle

     compile 'com.android.support:recyclerview-v7:23.3.0'

    Para más información sobre la importación de librerías en Eclipse ADT y Android Studio, consultar este artículo.

  2. Crear el layout para cada fila. En el ejemplo se mostrará una imagen (simulada mediante un círculo de color sólido) junto a un título y a un subtitulo. El background ?attr/selectableItemBackground aplicará un highlight a la selección del elemento.

    android recyclerview row

    <?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="wrap_content"
        android:background="?attr/selectableItemBackground"
        android:orientation="vertical"
        android:paddingBottom="@dimen/row_padding"
        android:paddingLeft="@dimen/general_padding"
        android:paddingRight="@dimen/general_padding"
        android:paddingTop="@dimen/row_padding">
    
        <View
            android:id="@+id/circleView"
            android:layout_width="@dimen/image"
            android:layout_height="@dimen/image"
            android:background="@drawable/circle"/>
    
        <TextView
            android:id="@+id/titleTextView"
            style="@style/RowTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_toEndOf="@+id/circleView"
            android:layout_toRightOf="@+id/circleView"
            android:paddingLeft="@dimen/row_padding" />
    
        <TextView
            android:id="@+id/subtitleTextView"
            style="@style/RowSubtitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/titleTextView"
            android:layout_alignStart="@+id/titleTextView"
            android:layout_below="@+id/titleTextView"
            android:paddingLeft="@dimen/row_padding" />
        
    </RelativeLayout>
    
  3. Implementar un RecyclerView.ViewHolder adecuado al layout de la fila que contendrá los atributos que permitan guardar las referencias a los distintos widgets del layout. En el constructor, donde recibimos un View que se corresponde con el layout de la fila, realizaremos el “mapeo” atributo-widget. Generalmente
    esta clase se define dentro del Adapter que veremos en el paso siguiente como una clase estática siempre y cuando no acceda a los miembros de su clase padre.

     class PaletteViewHolder extends RecyclerView.ViewHolder {
            private View circleView;
            private TextView titleTextView;
            private TextView subtitleTextView;
    
            public PaletteViewHolder(View itemView) {
                super(itemView);
                circleView = itemView.findViewById(R.id.circleView);
                titleTextView = (TextView) itemView.findViewById(R.id.titleTextView);
                subtitleTextView = (TextView) itemView.findViewById(R.id.subtitleTextView);
            }
    
            public TextView getTitleTextView() {
                return titleTextView;
            }
    
            public TextView getSubtitleTextView() {
                return subtitleTextView;
            }
    
            public View getCircleView() {
                return circleView;
            }
    
        }
    
  4. Implementar el RecyclerView.Adapter para el ViewHolder del paso anterior. En esta clase necesitaremos:
    • Guardar la referencia a la estructura de datos con los datos a mostrar en el listado que en nuestro caso será una lista de una clase Color. Esta lista se recibirá en el constructor.

      public class MaterialPaletteAdapter extends RecyclerView.Adapter<MaterialPaletteAdapter.PaletteViewHolder> {
          private List<Color> data;
      
          public MaterialPaletteAdapter(@NonNull List<Color> data) {
              this.data = data;
          }
      ...
      
    • En el método VH onCreateViewHolder (ViewGroup parent, int viewType) se devuelve una instancia de nuestro ViewHolder para la View que recibimos. En esta View tendremos que “inflar” el layout de la fila.
          @Override
          public PaletteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
              View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row, parent, false);
              return new PaletteViewHolder(row);
          }
      
    • public void onBindViewHolder (VH holder, int position) establece en el ViewHolder que recibe los datos correspondientes al elemento de la posición position.
          @Override
          public void onBindViewHolder(PaletteViewHolder holder, int position) {
              Color color = data.get(position);
              holder.getTitleTextView().setText(color.getName());
              holder.getSubtitleTextView().setText(color.getHex());
      
              GradientDrawable gradientDrawable = (GradientDrawable) holder.getCircleView().getBackground();
              int colorId = android.graphics.Color.parseColor(color.getHex());
              gradientDrawable.setColor(colorId);
          }
      
    • public abstract int getItemCount devuelve el número de elementos totales de la lista.
          @Override
          public int getItemCount() {
              return data.size();
          }
      

    El Adapter completo, incluyendo la implementación del ViewHolder como clase interna, queda tal que así:

    package com.danielme.android.recyclerview;
    
    import android.graphics.drawable.GradientDrawable;
    import android.support.annotation.NonNull;
    import android.support.v7.widget.RecyclerView;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    
    import java.util.List;
    
    /**
     * @author danielme.com
     */
    public class MaterialPaletteAdapter extends RecyclerView.Adapter<MaterialPaletteAdapter.PaletteViewHolder> {
        private List<Color> data;
    
        public MaterialPaletteAdapter(@NonNull List<Color> data) {
            this.data = data;
        }
    
        @Override
        public PaletteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row, parent, false);
            return new PaletteViewHolder(row);
        }
    
        @Override
        public void onBindViewHolder(PaletteViewHolder holder, int position) {
            Color color = data.get(position);
            holder.getTitleTextView().setText(color.getName());
            holder.getSubtitleTextView().setText(color.getHex());
    
            GradientDrawable gradientDrawable = (GradientDrawable) holder.getCircleView().getBackground();
            int colorId = android.graphics.Color.parseColor(color.getHex());
            gradientDrawable.setColor(colorId);
        }
    
        @Override
        public int getItemCount() {
            return data.size();
        }
    
        class PaletteViewHolder extends RecyclerView.ViewHolder {
            private View circleView;
            private TextView titleTextView;
            private TextView subtitleTextView;
    
            public PaletteViewHolder(View itemView) {
                super(itemView);
                circleView = itemView.findViewById(R.id.circleView);
                titleTextView = (TextView) itemView.findViewById(R.id.titleTextView);
                subtitleTextView = (TextView) itemView.findViewById(R.id.subtitleTextView);
            }
    
            public TextView getTitleTextView() {
                return titleTextView;
            }
    
            public TextView getSubtitleTextView() {
                return subtitleTextView;
            }
    
            public View getCircleView() {
                return circleView;
            }
    
        }
    }
    
  5. En el layout, incluir el RecyclerView. Simplemente lo posicionamos.
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:elevation="4dp"
            android:minHeight="?attr/actionBarSize"
            app:theme="@style/ThemeOverlay.AppCompat.ActionBar" />
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical" />
    
    </LinearLayout>
    
  6. En la Activity se establece el Adapter del RecyclerView
     RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
     recyclerView.setAdapter(new MaterialPaletteAdapter(colors));
    

    Hay que definir también el RecyclerView.LayoutManager, clase que indica cómo se va a dibujar en pantalla cada elemento de la lista. Android ya inluye tres implementaciones listas para ser utilizadas: LinearLayoutManager para un apilamiento vertical u horizontal, GridLayoutManager para una grid con celdas de igual tamaño y StaggeredGridLayoutManager para una grid con celdas de tamaño variable. Para el ejemplo queremos la primera en vertical (el constructor por defecto).

    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    

    Habitualmente querremos tener un separador o divisor de cada fila, especialmente si no usamos tarjetas que ya proporcionan de serie el borde del contenido. Dibujar este divisor, offset, borde, etc es tarea de RecyclerView.ItemDecoration. Por el momento no hay implementaciones oficiales, usaré una versión modificada de este código que permite utilizar el divisor propio del sistema o un drawable que definamos (véase este ejemplo). Ver código.

            recyclerView.addItemDecoration(new DividerItemDecoration(this));
    

    La clase completa queda así

    package com.danielme.android.recyclerview;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.support.v7.widget.Toolbar;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author danielme.com
     */
    public class MainActivity extends AppCompatActivity {
    
        private List<Color> colors;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
            setSupportActionBar(toolbar);
    
            initColors();
    
            RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
            recyclerView.setAdapter(new MaterialPaletteAdapter(colors));
            recyclerView.setLayoutManager(new LinearLayoutManager(this));
            recyclerView.addItemDecoration(new DividerItemDecoration(this));
    
        }
    
        private void initColors() {
            colors = new ArrayList<Color>();
    
            colors.add(new Color(getString(R.string.blue), getResources().getString(R.color.blue)));
            colors.add(new Color(getString(R.string.indigo), getResources().getString(R.color.indigo)));
            colors.add(new Color(getString(R.string.red), getResources().getString(R.color.red)));
            colors.add(new Color(getString(R.string.green), getResources().getString(R.color.green)));
            colors.add(new Color(getString(R.string.orange), getResources().getString(R.color.orange)));
            colors.add(new Color(getString(R.string.grey), getResources().getString(R.color.bluegrey)));
            colors.add(new Color(getString(R.string.amber), getResources().getString(R.color.teal)));
            colors.add(new Color(getString(R.string.deeppurple), getResources().getString(R.color.deeppurple)));
            colors.add(new Color(getString(R.string.bluegrey), getResources().getString(R.color.bluegrey)));
            colors.add(new Color(getString(R.string.yellow), getResources().getString(R.color.yellow)));
            colors.add(new Color(getString(R.string.cyan), getResources().getString(R.color.cyan)));
            colors.add(new Color(getString(R.string.brown), getResources().getString(R.color.brown)));
            colors.add(new Color(getString(R.string.teal), getResources().getString(R.color.teal)));
        }
    
    }
    
    

RecyclerViewListDemo

El evento OnItemClickListener

A nuestra lista le falta una funcionalidad que utilizamos prácticamente en todos los ListView: el evento click en una fila. Tendremos que implementar la captura del evento ya que RecyclerView no la proporciona (recordemos que sólo se encarga de deslizar y reciclar los items del adapter).

Tenemos por tanto libertad absoluta para implementar este evento dónde y cómo queramos. Generalmente se implementa en el ViewHolder ya que en esta clase tenemos acceso al método getAdapterPosition que devuelve la posición del item asociado al ViewHolder en la colección del Adapter con todos los items. Si se define en esta clase el evento onclick sabremos por tanto la posición del item pulsado, información que proporcionaba el AdapterView.OnItemClickListener del ListView.

Personalmente acostumbro a implementar el evento en la Activity y, puesto que el evento lo capturo en el ViewHolder, para poder atenderlo sigo los siguientes pasos:

  1. Nos aseguramos que el layout que muestra cada fila o item tiene como background un drawable con un selector de estados para aplicar un estilo distinto a la fila pulsada. Podemos aplicar directamente el que nos proporciona por defecto Android.
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/selectableItemBackground"
    
  2. Crear una interfaz con un método que recibe la View correspondiente a la fila pulsada y la posición del elemento en la lista. La implementación de este método es la acción a realizarse al pulsarse el elemento de la lista.
    package com.danielme.android.recyclerview.list;
    
    import android.view.View;
    
    public interface  RecyclerViewOnItemClickListener {
    
        void onClick(View v, int position);
    }
    
    
  3. El Adapter recibirá la implementación de la interfaz creada en el punto anterior y guardará la referencia para poder utilizarla dentro del ViewHolder.
    public class MaterialPaletteAdapter extends RecyclerView.Adapter<MaterialPaletteAdapter.PaletteViewHolder> {
        private List<Color> data;
        private RecyclerViewOnItemClickListener recyclerViewOnItemClickListener;
    
        public MaterialPaletteAdapter(@NonNull List<Color> data, @NonNull RecyclerViewOnItemClickListener recyclerViewOnItemClickListener) {
            this.data = data;
            this.recyclerViewOnItemClickListener = recyclerViewOnItemClickListener;
        }
    
  4. En el ViewHolder, se implementa la interfaz View.OnClickListener. Invocará al método de la interfaz RecyclerViewOnItemClickListener.
     public static class PaletteViewHolder extends RecyclerView.ViewHolder 
                implements View.OnClickListener{
            private View circleView;
            private TextView titleTextView;
            private TextView subtitleTextView;
    
            public PaletteViewHolder(View itemView) {
                super(itemView);
                circleView = itemView.findViewById(R.id.circleView);
                titleTextView = (TextView) itemView.findViewById(R.id.titleTextView);
                subtitleTextView = (TextView) itemView.findViewById(R.id.subtitleTextView);
                itemView.setOnClickListener(this);
            }
    
            public TextView getTitleTextView() {
                return titleTextView;
            }
    
            public TextView getSubtitleTextView() {
                return subtitleTextView;
            }
    
            public View getCircleView() {
                return circleView;
            }
    
            @Override
            public void onClick(View v) {
                recyclerViewOnItemClickListener.onClick(v, getAdapterPosition());
            }
        }
    
  5. En la Activity finalmente se implementa la respuesta al evento y se envía al Adapter. En el ejemplo se mostrará un Toast con la posición pulsada y con el color correspondiente.
     recyclerView.setAdapter(new MaterialPaletteAdapter(colors,new RecyclerViewOnItemClickListener() {
                @Override
                public void onClick(View v, int position) {
                    Toast toast = Toast.makeText(MainActivity.this, String.valueOf(position), Toast.LENGTH_SHORT);
                    int color = android.graphics.Color.parseColor(colors.get(position).getHex());
                    toast.getView().setBackgroundColor(color);
                    toast.show();
                }
            }));
    

recyclerview onitemclicklistener

Listas horizontales

Una limitación de ListView es la imposibilidad de crear una lista horizontal lo que nos obliga a implementarla nosotros mismos o bien recurrir a alguna librería externa. Ahora con RecyclerView se puede elegir la orientación de los datos en el scroll simplemente configurando el LinearLayoutManager que vimos anteriormente.

 recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));

Lamentablemente no se puede ajustar la altura del widget automáticamente con un wrap_content por lo que se asigna una altura fija en dp. Asimismo, hacemos que la barra de scroll se muestre en horizontal y para evitar que se superponga al divisor vertical se utiliza el estilo outsideInset

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="75dp"
        android:scrollbars="horizontal"
        android:scrollbarStyle="outsideInset" />

f

Con estos dos cambios tan sencillos ya tenemos el listado de colores en horizontal.

horizontal RecyclerView

Código de ejemplo

El proyecto de ejemplo para Android Studio se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

8 Responses to Android RecyclerView: Listas verticales y horizontales

  1. alfosno dice:

    me podrías proveer el colors.xml por favor

  2. Moisés Solís dice:

    muchísimas gracias, me sirvió mucho pero tengo una duda y me gustaría saber si por favor me puedes ayudar..
    ¿Es posible crear un recycler principal (lista categorías) y que ese mismo recycler pueda alterar el contenido de un segundo recycler (lista de categoría seleccionada)?
    Gracias de antemano!

  3. Mario dice:

    Hola Muchas gracias por tu aporte, es un excelente tutorial, me podrias ayudar, al cambiarle la orientación de la lista, el texto que tengo queda en una sola línea a lo largo y no como párrafo, me podrías ayudar con este tema. Quedo agradecido.

    • danielme.com dice:

      EL TextView muestra automáticamente todo el contenido de la cadena en más de una línea si hace falta siempre y cuando no se indique el atributo android:singleLine=”true” y tenga la suficiente altura para que quepan todas las líneas (por ejemplo con android:layout_height=”wrap_content”).

  4. Marcos Suarez dice:

    Excelente articulo. Estoy empesando con el RecyclerView y ListView. Estuve agregando mas componente como un EditText para poner una cantidad, resulta que tengo 50 item y al agregar un numero en el primer item al hacer un scroll para dirigirme a otro articulo el valor que puse al primer item se pasa a otro. quiere saber como mitigar ese problema. Gracias

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: