Crear Swipe Views En Android Con Tabs En La Action Bar

¿Te has preguntado como implementar pestañas en la Action Bar de tu aplicación Android, pero aún no encuentras el tutorial indicado?, si la respuesta es afirmativa, entonces te aseguro que este artículo te será de gran ayuda.

Quédate y podrás aclarar el uso de Tabs y la navegación horizontal en tus aplicaciones Android.

El conocimiento de este artículo ha quedado atrás, revisa la nueva forma de crear pestañas con el TabLayout.

Además terminarás creando una aplicación con el siguiente aspecto:

Puedes descargar el código de todo el proyecto desde el siguiente enlace:

Patrón de paginado horizontal en Android

El API de Android provee a los programadores muchas formas de navegación para implementar en sus aplicaciones. Una de ellas es el Paginado Horizontal o también llamado Swipe Views Pattern. Con este diseño el usuario cambia entre secciones de la aplicación con un gesto de arrastre horizontal en su dispositivo móvil. Veamos un ejemplo:
Swipe Gesture en la aplicación Contactos de Android
Además de la elegancia visual que proyectan los Swipe Views, también permiten una accesibilidad intuitiva y cómoda por parte del usuario al contenido principal de tu aplicación, mejorando la experiencia de su estadía.

Ejemplo de una Aplicación Android para un Restaurante

Como se vio al principio de este articulo, se ha creado una aplicación con unas secciones de pedidos en un Restaurante llamada “Foodsty”. Esta nos servirá de respaldo para aclarar el uso de Tabs en la Action Bar (Lee también Tutorial de la Action Bar en Android).

Lógicamente Foodsty consta de una actividad principal llamada Main junto a un archivo de diseño activity_main.xml. Adicionalmente se ha creado un archivo de diseño para los fragmentos que se usarán como ítems en el ViewPager, cuyo nombre es fragment_content.xml.

El objetivo de esta aplicación es mostrar una lista de cada uno de los productos disponibles por la tienda hipotética Foodsty, dependiendo de las tres categorías disponibles: “Platillos”, “Postres” y “Bebidas”.

El usuario podrá cambiar entres pestañas pulsando cada pestaña en la action bar (gesto tap) o arrastrando horizontalmente el contenido de las categorías (gesto swipe). Con estas características claras comencemos a crear nuestra aplicación Foodsty.

Usar un ViewPager con Fragmentos

Un ViewPager es un widget que permite mostrar contenido en páginas o secciones individuales. Para intercambiar entre elementos se usa el gesto swipe de izquierda a derecha o viceversa. Al implementarlo es necesario crear un adaptador del tipo PagerAdapter, que infle cada página de forma individual. Este proceso es muy parecido a cuando usábamos listas (Lee también Listas y Adaptadores en Android).

Dicho elemento pertenece a la Librería de soporte v4, así que no olvides incluir la dependencia en Gradle.
Debido a que la actividad principal de Foodsty está basada en un ViewPager, se debe implementar un nodo raíz del tipo <ViewPager>.

Veamos:

// Archivo activity_main.xml
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Añadir elementos a un ViewPager con un FragmentPagerAdapter

Lo siguiente es poblar nuestro ViewPager a través de un adaptador del tipo FragmentPagerAdapter. Esta subclase es una implementación especial para utilizar fragmentos en las páginas. ¿Por qué fragmentos?, porque así podrás tener el control del ciclo de vida del contenido, lo que te permite mayor flexibilidad en las operaciones que desees realizar.

Usa FragmentPagerAdapter, si con antelación conoces la cantidad de fragmentos que tendrá tu ViewPager. De lo contrario usa el adaptador FragmentStatePagerAdapter para indicar que la cantidad será variable. Esta clase te ayudará a liberar memoria de aquellos fragmentos que no tienen el foco.

Crear nuestro propio adaptador de fragmentos es muy simple. Solo debes tener claro los siguientes elementos básicos a sobrescribir:

  • Constructor: Es necesario crear un constructor que reciba el FragmentManager(Lee también Fragmentos en una aplicación Android) asociado al contexto donde se ejecuta el ViewPager.
  • getCount(): Sobrescribe este método para que retorne en la cantidad de elementos que tendrá tu pager.
  • getPageTitle(): Este método permite obtener el título de cada pestaña. Condiciona con un switch la posición de cada fragmento para asignar el nombre correcto.
  • getItem(): Este es el método que fabrica cada uno de los fragmentos de acuerdo a las características que hayas declarado.

Aclaradas estas especificaciones podemos ver la definición del adaptador FoodPagerAdapter:

/*
Adaptador de fragmentos para el ViewPager
 */
public static class FoodPagerAdapter extends FragmentPagerAdapter {

    public FoodPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public int getCount() {
        return 3;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        switch(position){
            case 0: return "PLATILLOS";
            case 1: return "POSTRES";
            case 2: return "BEBIDAS";
            default: return "";
        }
    }

    @Override
    public Fragment getItem(int i) {

        // Crear un FoodFragment con el nombre como argumento
        Fragment fragment = new FoodFragment();
        Bundle args = new Bundle();
        args.putString(FoodFragment.ARG_SECTION_NAME, getPageTitle(i).toString());
        args.putInt(FoodFragment.ARG_SECTION_IMAGE, i);
        fragment.setArguments(args);
        return fragment;

    }
}

Si prestas atención, FoodPagerAdapter es una clase estática anidada debido a que solo la usaremos en este contexto. Otra propiedad a destacar es que su método getItem() genera fragmentos del tipo FoodFragment, clase que veremos enseguida.

Adicionalmente envía como argumento el título del ítem y el id de la imagen de presentación que se usará. Por lo que se usan las constantes ARG_SECTION_NAME y ARG_SECTION_IMAGE.

Definición del Fragmento para el Adaptador

El contenido de cada página es un simple TextView informativo que muestra la pestaña actual y una imagen alusiva al contenido. El texto mostrado representa el tipo de elementos que alberga el fragmento, algo como “Lista de <TipoElementos>”.

Así que cada vez que el usuario cambie las pestañas, la cadena se irá actualizando en tiempo real para reflejar el estado.

Esclarecida esta idea, creamos un layout sencillo para ubicar estos dos elementos:

// Archivo fragment_content.xml
<?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">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="240dp"
        android:id="@+id/imageView"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:scaleType="fitXY" />

    <TextView
        android:id="@android:id/text1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="24sp"
        android:padding="32dp"
        android:layout_below="@+id/imageView"
        android:textColor="#c23"
        android:textStyle="bold" />

</RelativeLayout>

Ahora se declara la clase FoodFragment para representar el contenido:

/*
Fragmento que usaremos para cada pestaña
 */
public static class FoodFragment extends Fragment {

        public static final String ARG_SECTION_NAME = "section_name";
        public static final String ARG_SECTION_IMAGE = "section_image";

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

            Bundle args = getArguments();

            // Setear la imagen al fragmento
            ImageView image = (ImageView)rootView.
                    findViewById(R.id.imageView);
            image.setImageResource(imgIds[args.getInt(ARG_SECTION_IMAGE)]);

            // Setear el texto
            ((TextView) rootView.findViewById(android.R.id.text1)).setText(
                    getString(R.string.section_title)+" "+args.getString(ARG_SECTION_NAME));
            return rootView;
        }
    }

FoodFragment también es una clase estática interna. Aunque para este ejemplo solo se declaró esta clase, ya que se usará la misma estructura en las 3 pestañas, no significa que siempre debas proceder de esta forma. Crea diferentes clases para cada fragmento si estos son completamente distintos y maneja la situación con un switch.

Puedes crear un array global de tipo Fragment dentro del adaptador e inicializarlo en su constructor con los fragmentos de distinta clase. Con esa definición estas habilitado para usar los índices del array en el método getItem() en vez de preocuparte por sus tipos.

Añadir Tabs a la Action Bar

Lo primero que se debe hacer para crear pestañas dentro de la Action Bar es activar su modo de navegación por tabs. Para ello, obtén una instancia de la Action Bar y luego usa el método setNavigationMode(). Este recibe como parámetro la constante del modo de navegación, que en este caso será NAVIGATION_MODE_TABS:

// Obtener instancia de la Action Bar
final ActionBar actionBar = getActionBar();

// Activar el modo de navegación con tabs en la Action Bar
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

La lectura de eventos de las pestañas se realiza con la interfaz TabListener de la clase ActionBar. A cada pestaña se le debe asignar la escucha para que pueda responder a las instrucciones establecidas. Así que relacionaremos nuestra actividad principal con TabListener y sobrescribiremos los métodos de esta escucha:

public class MainActivity extends FragmentActivity implements ActionBar.TabListener {
  ...
  @Override
    public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
        // Nada por hacer
    }

    @Override
    public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
        // Coordinar la pestaña seleccionada con el item del viewpager
        viewpager.setCurrentItem(tab.getPosition());
    }

    @Override
    public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
        // Nada por hacer
    }
}

El método onTabSelected() se ejecuta cuando el usuario selecciona una pestaña, justo en ese momento actualizamos el contenido del ViewPager con setCurrentItem(), que recibe la posición del ítem a establecer en el foco. Esta posición se obtiene con getPosition() de la tab actualmente procesada.

onTabUnselected() se ejecuta cuando la pestaña sale del estado de selección y onTabReselected() es iniciado cuando el usuario selecciona una pestaña que actualmente se encuentra seleccionada.
Por último se crean 3 tabs con el método newTab() y se añaden con addTab():

// Añadir 3 pestañas y asignarles un título y escucha
for (int i = 0; i < adapter.getCount(); i++) {
    actionBar.addTab(
            actionBar.newTab()
                    .setText(adapter.getPageTitle(i))
                    .setTabListener(this));
}

Usa setText() para añadir el título de cada una con respecto al título del elemento correspondiente del adaptador y relaciona la escucha implementada con setTabListener().

Coordinar los elementos del ViewPager con las Tabs

Hasta ahora el usuario puede hacer tap en las pestañas y ver el contenido, pero aún no puede usar el gesto swipe. Por eso implementaremos la interfaz OnPageChangeListener sobre nuestro pager. Con ella podemos sincronizar el cambio de pestañas cuando se cambie las páginas, es decir, el efecto contrario a cuando implementamos TabListener.

viewpager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
    // Coordinar el item del pager con la pestaña
    actionBar.setSelectedNavigationItem(position);
}
});

Usa SimpleOnPageChangeListener si deseas implementar solo los métodos necesarios de OnPageChangeListener.
Como ves, se usa el método setOnPageChangeListener() para añadir la escucha.

El método onPageSelected() se ejecuta cuando la página es seleccionada, por lo que se ordena en ese instante que la action bar cambie la pestaña a la posición determinada por el parámetro de entrada position.

Finalmente junta todas las declaraciones y compone el método onCreate() de la actividad principal:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Obtener instancia de la Action Bar
    final ActionBar actionBar = getActionBar();

    // Activar el modo de navegación con tabs en la Action Bar
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

    // Deshabilitar el caret Up del icono de la aplicación
    actionBar.setHomeButtonEnabled(false);

    // Crear adaptador de fragmentos
    adapter = new FoodPagerAdapter(getSupportFragmentManager());

    // Obtener el ViewPager y setear el adaptador y la escucha
    viewpager = (ViewPager) findViewById(R.id.pager);
    viewpager.setAdapter(adapter);
    viewpager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            // Coordinar el item del pager con la pestaña
            actionBar.setSelectedNavigationItem(position);
        }
    });

    // Añadir 3 pestañas y asignarles un título y escucha
    for (int i = 0; i < adapter.getCount(); i++) {
        actionBar.addTab(
                actionBar.newTab()
                        .setText(adapter.getPageTitle(i))
                        .setTabListener(this));
    }
}

Hasta este momento la aplicación se vería de esta forma:
Aplicación Android de un Restaurante con solo texto en las Tabs
Pero si tu eres más fino y deseas usar iconos en cada pestaña, entonces buscas los drawables correspondientes y empleas el método setIcon() para asignarlos.

//Las referencias de tus iconos
int idIcons[] = {R.drawable.icon1, R.drawable.icon2, R.drawable.icon3};

// Añadir 3 pestañas y asignarles un título y escucha
for (int i = 0; i < adapter.getCount(); i++) {
    actionBar.addTab(
            actionBar.newTab()
                    .setIcon(idIcons[i])
                    // Habilita el titulo si lo prefieres
                    // .setText(adapter.getPageTitle(i))
                    .setTabListener(this));
}

Con estos cambios la aplicación mutaría de esta forma:
Aplicación Android de un Restaurante con iconos en las Tabs
Android Studio proporciona un método automático para generar una actividad con pestañas más el uso de Swipe Views con un ViewPager. Solo debes ubicarte en tu paquete java y dar click derecho, luego eliges New > Activity > Tabbed Activity.
Tabbed Activity en Android Studio

Bonus: Implementación de un Title Strip en Android

Los Title Strips crea pestañas dinámicas en un ViewPager. Se asemejan mucho a las pestañas de la action bar, solo que estos no tienen interacción con el usuario. Además un Title Strip muestra en tiempo real los ítems anteriores, el actual y los siguientes, a diferencia de las tabs que se mantienen fijas.

Su implementación es muy sencilla, solo anidas un elemento del tipo <android.support.v4.view.PagerTitleStrip> dentro del elemento <ViewPager>. Cada pestaña toma el nombre de la página correspondiente con el método getPageTitle(), así que no debes preocuparte por implementarlo programáticamente.

Si deseas reemplazar las tabs por un PagerTitleStrip entonces elimina todas las implementaciones de la action bar y agrega el elemento al archivo de diseño:

// Archivo activity_main.xml con PagerTitleStrip
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.PagerTitleStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#c23"
        android:textColor="#fff"
        android:paddingTop="4dp"
        android:paddingBottom="4dp" />

</android.support.v4.view.ViewPager>

Este diseño produciría el siguiente resultado:

Como alternativa, puedes ajustar el atributo layout_gravity para que el Title Strip gravite en la parte inferior “bottom”.

Un dato más. También es posible usar un Tab Strip con un indicador interactivo y visual, cuyo nombre es PagerTabStrip. Este View utiliza una pequeña barra lateral que se desliza de un ítem a otro para representar la interacción con las páginas.

Si quieres implementarlo solo cambia por el elemento <PagerTabStrip> en tu archivo de diseño:

Consideraciones Finales

  • En la nueva API 21 los modos de navegación de la action bar han quedado obsoletos, por lo cual el uso de pestañas en la Action Bar no son recomendables en el uso de aplicaciones para Android Lollipop, pero aún podemos usarlas en versiones anteriores. Recuerda que la propagación de un nuevo sistema operativo requiere cierto tiempo para dominar considerablemente una cuota de mercado. No obstante, el centro de desarrollo de Android recomienda el uso del componente SlidingTabLayout para la creación de pestañas.
  • También es importante saber que en el API 21 se ha creado un nuevo widget llamado Toolbar, el cual puede reemplazar la Action Bar. Este nuevo componente es muy flexible a la hora de modificar su aspecto y agregarle estilo (Hablaremos sobre él en futuros artículos).
  • Para la creación del estilo de Foodsty se usó la herramienta de código abierto Android Action Bar Style Generator de Jeff Gilfelt. Con ella puedes modificar el estilo de cada componente de la action bar, como el background, la transparencia, el tema padre, etc y producir todos los drawables necesarios para generar un tema global de la aplicación (Lee también Diseñar Temas y Estilos para tus Aplicaciones Android).

Iconos cortesía de IconFinder. Imágenes cortesía de Freepik.