Inicio Android Actionbar / Appbar / Toolbar en Android (III): Filtros y Tabs

Actionbar / Appbar / Toolbar en Android (III): Filtros y Tabs

por sgoliver

En los dos artículos anteriores aprendimos a hacer uso de la funcionalidad básica de una action bar y utilizar el nuevo componente Toolbar para conseguir el mismo comportamiento e incluso extenderlo a otras partes de la interfaz.

En este tercer artículo sobre el tema vamos a ver dos métodos de navegación aplicables a nuestras aplicaciones y que están íntimamente relacionados con la action bar. El primer de ellos, el más sencillo, será utilizar un filtro (page filter), o para entendernos mejor, una lista desplegable integrada en la app bar.

El segundo método, algo más laborioso aunque nos ayudaremos de algunas clases ya existentes, consiste en utilizar pestañas (tabs) bajo la action bar. Estas pestañas, a su vez, podrán ser fijas o deslizantes.

Empecemos por la primera de las alternativas: el uso de listas desplegables (spinner) integradas en la action bar. Esto no debería tener ningún misterio para nosotros con lo que ya llevamos aprendido en artículos anteriores. En primer lugar ya dijimos que el nuevo control Toolbar se comporta como cualquier otro contenedor, en el sentido de que puede contener a otros controles. Por tanto, podemos empezar por añadir a nuestra aplicación un nuevo Toolbar que contenga un control Spinner.

<androidx.appcompat.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="?attr/actionBarSize"
    android:layout_width="match_parent"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:elevation="4dp"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light" >

    <Spinner android:id="@+id/cmbToolbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</androidx.appcompat.widget.Toolbar>

Igual que ya explicamos en el artículo anterior, incluiremos este código en un fichero independiente /res/layout/toolbar.xml, que añadiremos al layout principal mediante la cláusula <include>.

Para que la lista desplegable no desentone con el estilo y colores de nuestra Toolbar vamos a personalizar los layouts específicos de los elementos de la lista desplegable y del control en sí, como ya explicamos en el artículo dedicado al control Spinner.

Para ello, definiremos en primer lugar el layout personalizado para el control (/res/layout/appbar_filter_title.xml), donde lo único destacable es que utilizará el mismo estilo estándar que el utilizado para el título de una action bar:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Y en segundo lugar definiremos el layout de los elementos de la lista desplegable (/res/layout/appbar_filter_list.xml), donde utilizaremos como color de fondo y color de texto los estándar del tema Material Light:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    style="?android:attr/spinnerDropDownItemStyle"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@color/background_material_light"
    android:textColor="@color/primary_text_default_material_light" />

Por último, asociaremos todos estos elementos al spinner desde el método onCreate() de nuestra actividad, y por supuesto crearemos y asignaremos algún adaptador con las opciones seleccionables. Todo esto ya lo explicamos con detalle en el artículo sobre el control Spinner, por lo que no me detendré más en ello. Tan sólo indicar que en mi caso sólo añadiré a la lista 3 opciones de ejemplo mediante un ArrayAdapter sencillo:

class MainActivity : AppCompatActivity() {
    private lateinit var cmbToolbar : Spinner

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tbar = findViewById<Toolbar>(R.id.appbar)
        setSupportActionBar(tbar)
        supportActionBar?.setDisplayShowTitleEnabled(false);

        cmbToolbar = findViewById(R.id.cmbToolbar)

        val datos = arrayOf("Opción 1", "Opción 2", "Opción 3")
        val adaptador =
            ArrayAdapter(this, R.layout.appbar_filter_title, datos)

        adaptador.setDropDownViewResource(
            R.layout.appbar_filter_list)

        cmbToolbar.adapter = adaptador

        cmbToolbar.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
            override fun onNothingSelected(parent: AdapterView<*>) {
                //... Acciones al no existir ningún elemento seleccionado
            }
            override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
                //... Acciones al seleccionar una opción de la lista
                Log.i("Appbar 3", "Seleccionada opción " + position);
            }
        }
    }
}

Tas solo con esto tenemos ya listo nuestro filtro integrado en la action bar, con el aspecto de las imágenes mostradas al principio del artículo. Ya sólo quedaría responder a la selección de cada elemento de la lista, dentro del evento onItemSelected(), con las acciones necesarias según la aplicación, por ejemplo, manipulando el resto de datos mostrados en la interfaz, o intercambiando fragments como veremos en breve para el caso de la navegación por pestañas.

Vamos ahora con el segundo de los métodos de navegación indicados, las pestañas o tabs. Las pestañas suelen ir colocadas justo bajo la action bar, y normalmente con el mismo color y elevación. Además, debemos poder alternar entre ellas tanto pulsando sobre la pestaña elegida como desplazándonos entre ellas con el gesto de desplazar el dedo a izquierda o derecha sobre el contenido. Nota: Por este mismo motivo, cuando nuestro contenido interactúe también con gestos de este tipo (por ejemplo un mapa) no deberíamos utilizar pestañas en la interfaz.

Para conseguir esto, vamos a distinguir como mínimo 3 zonas básicas en la interfaz:

  1. La action bar.
  2. El bloque de pestañas.
  3. El contenido de cada pestaña, que deberá ir integrado en algún contenedor que nos permita alternar entre ellas deslizando en horizontal.

El primer punto ya lo vimos en detalle en el artículo anterior, por lo que no nos repetiremos. Para el segundo nos vamos a ayudar del componente TabLayout, contenido en la nueva librería de material design. Y para el último punto utilizaremos el componente ViewPager, que nos permitirá alternar entre fragments con el gesto de deslizamiento (por tanto, el contenido de cada pestaña lo incluiremos en fragments independientes).

Para hacer uso de la nueva librería de material design debemos en primer lugar añadir su referencia al fichero build.gradle, de la siguiente forma:

dependencies {
    //...
    implementation 'com.google.android.material:material:1.1.0'
    //...
}

Hecho esto, ya podremos utilizar en nuestro layout el nuevo componente TabLayout, que nos servirá para albergar el grupo de pestañas. Y con esto no me refiero al contenido de las pestañas, sino a las pestañas en sí. Utilizar este componente es muy sencillo, en principio basta con incluirlo en nuestro layout XML, sin asignar ninguna propiedad específica salvo los ya habituales widthheight e id. El resto de la configuración la haremos desde el código kotlin.

<LinearLayout 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:orientation="vertical"
    tools:context=".MainActivity">

    <include android:id="@+id/appbar"
        layout="@layout/toolbar" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/appbartabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white" />

</LinearLayout>

Si revisáis el código anterior veréis que en principio es bastante sencillo. Usamos como contenedor principal un LinearLayout vertical, en el que incluimos en primer lugar el Toolbar (la definición concreta es idéntica a la que vimos en el artículo anterior), tras éste incluimos el TabLayout, que contendrá el grupo de pestañas, y por último el ViewPager que contendrá los fragments de cada pestaña y permitirá la navegación entre ellas con el gesto de deslizamiento.

Definido el layout XML nos toca empezar a inicializar y configurar todos estos componentes en el código kotlin de la aplicación.

Empezaremos por el ViewPager. Este componente va a basar su funcionamiento en un adaptador, de forma similar a los controles tipo lista, aunque en esta ocasión será algo más sencillo. Como dijimos antes, el contenido de cada pestaña lo incluiremos en fragments independientes y el ViewPager se encargará de permitirnos alternar entre ellos. Para esto, deberemos construir un adaptador que extienda de FragmentPagerAdapter. En este adaptador tan sólo tendremos que implementar su constructor, y sobrescribir los métodos getCount()getItem(pos) y getPageTitle(). Los nombres son bastante ilustrativos, el primero se encargará de devolver el número total de pestañas, el segundo devolverá el fragment correspondiente a la posición que reciba como parámetro, y el último devolverá el título de cada pestaña.

Para no alargar mucho el ejemplo, en mi caso voy a incluir 6 pestañas para que pueda comprobarse bien el deslizamiento, pero sólo crearé 2 fragments para los contenidos, por lo que el contenido se repetirá en muchas de ellas. En una situación normal cada pestaña tendría su contenido y por tanto su fragment independiente (o al menos filtrado o personalizado de algún modo).

Veamos cómo quedaría la implementación del adaptador, que es bastante directa según lo ya explicado:

class MiFragmentPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    companion object {
        const val PAGE_COUNT = 6
    }

    private val tabTitles = arrayOf("Tab Uno", "Tab Dos", "Tab Tres", "Tab Cuatro", "Tab Cinco", "Tab Seis")

    override fun getCount(): Int {
        return PAGE_COUNT
    }

    override fun getItem(position: Int): Fragment {
        return when(position) {
            0,2,4 -> Fragment1()
            else -> Fragment2()
        }
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return tabTitles[position]
    }
}

Los nombres de cada pestaña los almaceno en un array clásico. Por su parte, las clases Fragment1 y Fragment2 son fragments muy sencillos, que muestran simplemente una etiqueta de texto con su nombre. Puede consultarse su código y su layout en el repositorio GitHub del curso.

Para asociar este adaptador al componente viewpager llamaremos a su propiedad adapter desde el onCreate() de nuestra actividad principal:

class MainActivity : AppCompatActivity() {
    private lateinit var cmbToolbar : Spinner

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tbar = findViewById<Toolbar>(R.id.appbar)
        setSupportActionBar(tbar)

        val viewPager = findViewById<ViewPager>(R.id.viewpager)
        viewPager.adapter = MiFragmentPagerAdapter(supportFragmentManager)

        //...
    }
}

Seguimos ahora con el TabLayout, tras obtener la referencia al control, tendremos que indicar el tipo de pestañas que queremos utilizar, entre fijas (TabLayout.MODE_FIXED) o deslizantes (TabLayout.MODE_SCROLLABLE), mediante su propiedad tabMode. La modalidad de pestañas fijas (pestañas no deslizantes, todas del mismo tamaño) no debería utilizarse cuando el número de pestañas excede de 3 o 4, ya que probablemente la falta de espacio haga que los títulos aparezcan incompletos, no siendo nada buena la experiencia de usuario.

Por último, sólo nos quedará enlazar nuestro ViewPager con el TabLayout, lo que conseguiremos llamando al método setupWithViewPager(). Este último método hace bastante trabajo por nosotros, ya que se encargará de:

  1. Crear las pestañas necesarias en el TabLayout, con sus títulos correspondientes, a partir de la información proporcionada por el adaptador del ViewPager.
  2. Asegurar el cambio de la pestaña seleccionada en el TabLayout cuando el usuario se desplaza entre los fragments del ViewPager. Para ello define automáticamente el listener ViewPager.OnPageChangeListener.
  3. Y asegurar también la propagación de eventos en «sentido contrario», es decir, asegurará que cuando se selecciona directamente una pestaña del TabLayout, el ViewPager muestra el fragment correspondiente. Para ello definirá el listener TabLayout.OnTabSelectedListener.

Es importante tener todo esto en cuenta ya que si necesitamos personalizar de alguna forma la respuesta a estos eventos, o no estamos utilizando un ViewPager, quizá tengamos que hacer este trabajo por nosotros mismos. Si se diera este caso, tenéis disponible los métodos newTab() y addTab() del TabLayout con el que podréis crear y añadir manualmente las pestañas al control.

Veamos cómo quedaría todo esto dentro del onCreate() de la actividad principal:

class MainActivity : AppCompatActivity() {
    private lateinit var cmbToolbar : Spinner

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //...

        val tabLayout = findViewById<TabLayout>(R.id.appbartabs)
        tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
        tabLayout.setupWithViewPager(viewPager)
    }
}

Si ejecutamos la aplicación en este momento deberíamos obtener un resultado muy parecido al mostrado al comienzo del artículo.

Como puede comprobarse, todo funciona correctamente salvo por el hecho de que las pestañas no comparten el color de la action bar superior. Esto tiene fácil solución haciendo uso de otro de los nuevos componentes presentados con la nueva librería de material design, el AppBarLayout. Este nuevo tipo de layout tendrá especial interés de cara a facilitar la tarea de animar tanto la action bar como sus componentes relacionados (por ejemplo las pestañas) como respuesta a otros eventos, como el desplazamiento en una lista. Esto lo veremos en un futuro artículo, pero por el momento ya nos va a servir también para hacer más consistente el aspecto de nuestras pestañas con la action bar de la actividad y con el tema definido para la aplicación.

Para ello bastará con incluir el Toolbar y el TabLayout de nuestra interfaz dentro de un elemento AppBarLayout. En este nuevo AppBarLayout tan sólo asignaremos la propiedad android:theme, tal como ya vimos en el artículo anterior sobre el control Toolbar.

<LinearLayout 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:orientation="vertical"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbarlayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" >

        <include android:id="@+id/appbar"
            layout="@layout/toolbar" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/appbartabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white" />

</LinearLayout>

Lo único a tener en cuenta, para evitar efectos indeseados, es que debemos eliminar la asignación del atributo elevation del control Toolbar. El nuevo AppBarLayout se encargará de establecer la elevación estándar al conjunto Toolbar+TabLayout y mostrar la sombra correspondiente sin necesidad de añadir elementos adicionales.

Si volvemos a ejecutar ahora la aplicación, el resultado tendrá ya el aspecto y funcionamiento deseados, las pestañas utilizarán el mismo color que la action bar (primary color) y el indicador de la pestaña seleccionada utilizará por defecto el accent color definido en el tema:

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la pagina del curso en GitHub.

También te puede interesar

11 comentarios

Daniel 17/06/2015 - 20:46

se pueden poner iconos en vez de titulos?
gracias :D

Responder
Jose 26/06/2015 - 23:24

Intenté correr la app en Eclipse… pero me dieron muchos errores.. y tengo la duda de si solo podría correrla en Android studio?? Necesito integrar librerías para poder hacerlo en Eclipse???

Gracias de antemano por la respuesta.

Responder
Víctor 07/07/2015 - 16:01

Hola, antes de nada darte las gracias por este maravilloso tutorial, más de una vez me has sacado las castañas del fuego jejeje. Te quería comentar un problemilla que me ha surgido y que no se si es que no he encontrado la forma de solucionarlo o es que directamente not tiene solución.
Tengo un layout como en tu ejemplo, con un TabLayout y un ViewPager, hasta ahí todo correcto, el tema es que en mi caso solo tengo dos Tab y no consigo hacer que cada tab ocupe la mitad de la pantalla. ¿Hay alguna forma?

Un saludo y muchas gracias.

Responder
Edgardo 20/07/2015 - 5:26

Al usar:

adapter = new ViewPagerAdapter(getActivity().getSupportFragmentManager());

viewPager.setAdapter(new MiFragmentPagerAdapter(getSupportFragmentManager()));

en onCreateView con diferentes fragments la segunda vez que se quiere volver a un tab no recrea la vista, pero esto se puede solucionar utilizando:

viewPager.setAdapter(new MiFragmentPagerAdapter(getChildFragmentManager()));

Espero le pueda servir de ayuda a alguien.

Saludos

Responder
d 12/09/2015 - 18:18

Buenas,

¡gracias por este fantástico tutorial!. Lo he probado, y funciona, pero al cargar veo ‘false’ en cada uno de los frames, eliga el que eliga, en vez de mostrar el texto en cada uno de ellos. ¿Podrías ayudarme?

Gracias!

Responder
antonio siles 17/09/2015 - 19:25

Hola sgoliver, primero felicitarlo por su excelente tutorial, tenia una pregunta ,
¿Queria saber si hay una manera de colocar iconos en vez de textos ??
Le agradezco la ayuda de antemano

Responder
Rafel 02/10/2015 - 20:30

Fantastico tutorial y muchas gracias, pero no lo consigo.
Como puedo hacer que me abra fragments con el Spinner directamente?

Responder
Santiago Montoya 07/01/2016 - 12:21

Muy buen tutorial,
solo una pregunta, ¿Cómo se podrían internacionalizar los títulos de los Tabs?
los pregunto por que al ponerlos directo en un String[] en una clase que no tiene acceso a los Resources no se como seria… depronto pasando un Context en el constructor, ¿no?

Responder
Raúl López 30/03/2016 - 21:20

Una vez que obtienes la referencia al control, puedes crear las pestañas directamente invocando el método addTab(). Ahí si ya podrías usar los recursos del sistema, como por ejemplo cualquier cadena de strings:

Ejemplo.

tabLayout.addTab(tabLayout.newTab().setText(getString(R.string.titulo_tab1)));

Un saludo

Responder
Lucas Michailian 02/07/2016 - 17:26

Hola! Excelentes tutos ! Una pregunta , es posible poner el indicador (underline) por encima del tab ??

Gracias!

Responder
Mario German 09/08/2016 - 19:18

Gracias por compartir tu conocimiento con todos, te queria preguntar como puedo colocar un spinner dentro de un recyclcerview, cuentas con algun tutorial o link al respecto, te agradeceria demasiado.

Responder

Dejar un comentario

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies