Android

Tabs dinámicas en Android – ViewPager y TabLayout

Las pestañas o tabs en Android permiten mostrar el contenido en pestañas, a las cuales se puede navegar haciendo click en el título de la pestaña o arrastrando hacia la izquierda o la derecha.

Hoy veremos cómo tener pestañas dinámicas en Android, es decir, poder agregar Tabs infinitas a una lista conforme el usuario las requiera.

En la parte inferior existen pestañas o tabs dinámicas que se agregan cuando el usuario hace click en la última pestaña

Lo que vamos a usar será un adaptador personalizado, un TabLayout y un ViewPager.

Nota: asumo que ya sabes un poco sobre Tabs estáticas y esas cosas, pues aquí no daré un repaso sobre eso.

Primero: el layout principal

Tenemos dos elementos: el ViewPager para mostrar el contenido de la Tab y el TabLayout, para las pestañas dinámicas en Android

Este layout puede ser un fragmento o una actividad, y cuenta con dos vistas:

  • La vista en donde se muestra el contenido de la pestaña
  • La vista en donde se muestran las pestañas

En mi caso lo puse en un ConstraintLayout que se ve así:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabLayoutAvios"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tabMode="scrollable" />

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPagerAvios"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:layout_weight="1"
        app:layout_constraintBottom_toTopOf="@+id/tabLayoutAvios"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

El ViewPager tiene el id viewPagerAvios y el TabLayout el id tabLayoutAvios, por diseño, el TabLayout debería ir arriba del ViewPager pero los requerimientos de la app fueron así.

El fragmento que se muestra dentro del ViewPager

Antes de que veamos cómo agregar una Tab a un TabLayout de manera dinámica debemos saber que cada Tab se relaciona con un fragmento.

Este fragmento puede ser distinto para cada Tab, o puede ser el mismo. Yo he usado el mismo, que es un formulario cuyo diseño es así:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_lugares"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginBottom="8dp"
        android:text="Guardar"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:text="Identificador:"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:text="CC001"
        app:layout_constraintStart_toEndOf="@+id/textView3"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:ems="10"
        android:hint="Tipo de prenda"
        android:inputType="textPersonName"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <CheckBox
        android:id="@+id/checkBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="16dp"
        android:text="¿Trazo?"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editText" />

    <CheckBox
        android:id="@+id/checkBox2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="¿Muestra?"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/checkBox" />

    <EditText
        android:id="@+id/editText3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:ems="10"
        android:hint="Nombre"
        android:inputType="textPersonName"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/checkBox2" />

    <EditText
        android:id="@+id/editText4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:ems="10"
        android:hint="Cantidad"
        android:inputType="textPersonName"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editText3" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="Agregar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editText4" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:text="Quitar"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</android.support.constraint.ConstraintLayout>

En su código Java todavía no tengo nada pero se ve así:

package mx.com.textitez.textitezapp;


import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


/**
 * A simple {@link Fragment} subclass.
 */public class FragmentFormularioDeAvio extends Fragment {


    public FragmentFormularioDeAvio() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_formulario_de_avio, container, false);
    }

}

Dentro de este código se debe manejar la interacción con el contenido del mismo, es decir, este código no sabe nada sobre las tabs, está aislado y solo se encarga de inflar el contenido del fragmento.

Agregar pestañas o tabs iniciales

Al inicio debemos tener dos tabs: una que tiene el número 1, y la otra, que muestra el icono para agregar una tab.

Nota: yo uso un fragmento, no una actividad, la diferencia se nota por ejemplo en view.findViewById.

En la parte superior definimos nuestros elementos:

private TabLayout tabLayout;
private ViewPager viewPager;
AdaptadorViewPager adaptadorViewPager;

El adaptador es un adaptador personalizado:

class AdaptadorViewPager extends FragmentPagerAdapter {
        private final List<Fragment> listaDeFragmentos = new ArrayList<>();
        private final List<String> listaDeTitulosDeFragmentos = new ArrayList<>();

        AdaptadorViewPager(FragmentManager manager) {
            super(manager);
        }

        @Override
        public Fragment getItem(int position) {
            return listaDeFragmentos.get(position);
        }

        @Override
        public int getCount() {
            return listaDeFragmentos.size();
        }

        void agregarFragmento(Fragment fragment, String title) {
            listaDeFragmentos.add(fragment);
            listaDeTitulosDeFragmentos.add(title);
        }

        // Si es el título de la última pestaña, regresamos null, lo
        // cual regresará el icono únicamente
        @Override
        public CharSequence getPageTitle(int position) {
            if (position >= getCount() - 1) return null;
            return listaDeTitulosDeFragmentos.get(position);
        }
    }

Extiende de FragmentPagerAdapter, simplemente sobrescribimos unos métodos. Internamente tenemos dos listas: una lista de los fragmentos y otra de los títulos.

Dentro del método getPageTitle devolvemos null si es la última pestaña, esto es para forzar que se muestre el icono.

Ahora obtenemos una referencia a los mismos y ponemos un listener al TabLayout:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    viewPager = view.findViewById(R.id.viewPagerAvios);
    tabLayout = view.findViewById(R.id.tabLayoutAvios);
    adaptadorViewPager = new AdaptadorViewPager(getActivity().getSupportFragmentManager());
    tabLayout.setupWithViewPager(viewPager);
    agregarTabsIniciales(viewPager);
    agregarIconoAUltimoTab();
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            if (tab.getPosition() == viewPager.getAdapter().getCount() - 1) {
                agregarTab();
            }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
        }
    });

}

En la línea 3, 4 y 5 obtenemos una referencia a nuestros elementos XML. En la línea 6 configuramos el ViewPager del TabLayout, simplemente indicando la variable.

En la línea 7 y 8 tenemos dos métodos interesantes que nos ayudarán. Vamos a verlos:

El método agregar tabs iniciales

private void agregarTabsIniciales(ViewPager viewPager) {
    adaptadorViewPager = new AdaptadorViewPager(getActivity().getSupportFragmentManager());
    for (int x = 1; x < 3; x++) {
        adaptadorViewPager.agregarFragmento(new FragmentFormularioDeAvio(), String.valueOf(x));
    }
    viewPager.setAdapter(adaptadorViewPager);
}

Simplemente agrega dos pestañas al ViewPager, fíjate en la línea 4 pues ahí se crea un nuevo Fragmento del formulario, cuyo título será el número de pestaña.

Recuerda que el código de FragmentFormularioDeAvio ya lo vimos arriba, es el que se muestra dentro de cada pestaña.

El método para poner el icono a la última Tab

Ahora veamos el otro método que se encarga de establecer el icono de la última pestaña. Queda así:

private void agregarIconoAUltimoTab() {
    TabLayout.Tab tab = tabLayout.getTabAt(viewPager.getAdapter().getCount() - 1);
    if (tab != null) {
        tab.setIcon(R.drawable.ic_add_white_24dp);
    }
}

Obtenemos la última Tab invocando a getTabAt y pasándole el número de tabs que ya tenemos menos 1.

Si no es null (no queremos NullPointerException) entonces le ponemos un icono. Si en tu caso no existe, es porque no tienes un icono así, simplemente coloca uno que sí hayas agregado.

Agregar pestaña si se toca la última

Recuerda que tenemos el siguiente código:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    viewPager = view.findViewById(R.id.viewPagerAvios);
    tabLayout = view.findViewById(R.id.tabLayoutAvios);
    adaptadorViewPager = new AdaptadorViewPager(getActivity().getSupportFragmentManager());
    tabLayout.setupWithViewPager(viewPager);
    agregarTabsIniciales(viewPager);
    agregarIconoAUltimoTab();
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            if (tab.getPosition() == viewPager.getAdapter().getCount() - 1) {
                agregarTab();
            }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
        }
    });

}

En la línea 12, si se toca la última pestaña se agrega un Tab. El código de esa función es el siguiente:

private void agregarTab(){
    // el título del tab
    String titulo = String.valueOf(viewPager.getAdapter().getCount() + 1);
    adaptadorViewPager.agregarFragmento(new FragmentFormularioDeAvio(), titulo);
    adaptadorViewPager.notifyDataSetChanged();
    agregarIconoAUltimoTab();
    // Enfocar la penúltima 100 milisegundos después
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            // Seleccionar la penúltima pestaña
            tabLayout.setScrollPosition(viewPager.getAdapter().getCount() - 2, 0, true);
            viewPager.setCurrentItem(viewPager.getAdapter().getCount() - 2);
        }
    }, 100);
}

Simplemente invocamos al método agregarFragmento que ya vimos antes en el ciclo for.

Es importante invocar al método notifyDataSetChanged para que el ViewPager sepa que cambiaron los datos; si no hacemos esto, se generará una excepción.

Después volvemos a colocar el icono del último tab y finalmente esperamos 100 milisegundos para enfocar la pestaña que va antes de la penúltima, esto de la espera es porque cuando se agrega una pestaña se selecciona por defecto la primera, así que debemos esperar un poco a que se termine y luego seleccionar la nuestra.

Poniendo todo junto

Ahora que ya he explicado el código, veámoslo como uno solo:

import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

/**
 * A simple {@link Fragment} subclass.
 */public class FragmentAvios extends Fragment {

    private TabLayout tabLayout;
    private ViewPager viewPager;
    AdaptadorViewPager adaptadorViewPager;

    public FragmentAvios() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_fragment_avios, container, false);
    }

    private void agregarIconoAUltimoTab() {
        TabLayout.Tab tab = tabLayout.getTabAt(viewPager.getAdapter().getCount() - 1);
        if (tab != null) {
            tab.setIcon(R.drawable.ic_add_white_24dp);
        }
    }

    private void agregarTab(){
        // el título del tab
        String titulo = String.valueOf(viewPager.getAdapter().getCount() + 1);
        adaptadorViewPager.agregarFragmento(new FragmentFormularioDeAvio(), titulo);
        adaptadorViewPager.notifyDataSetChanged();
        agregarIconoAUltimoTab();
        // Enfocar la penúltima 100 milisegundos después
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                // Seleccionar la penúltima pestaña
                tabLayout.setScrollPosition(viewPager.getAdapter().getCount() - 2, 0, true);
                viewPager.setCurrentItem(viewPager.getAdapter().getCount() - 2);
            }
        }, 100);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        viewPager = view.findViewById(R.id.viewPagerAvios);
        tabLayout = view.findViewById(R.id.tabLayoutAvios);
        adaptadorViewPager = new AdaptadorViewPager(getActivity().getSupportFragmentManager());
        tabLayout.setupWithViewPager(viewPager);
        agregarTabsIniciales(viewPager);
        agregarIconoAUltimoTab();
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                if (tab.getPosition() == viewPager.getAdapter().getCount() - 1) {
                    agregarTab();
                }
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {
            }
        });

    }

    private void agregarTabsIniciales(ViewPager viewPager) {
        adaptadorViewPager = new AdaptadorViewPager(getActivity().getSupportFragmentManager());
        for (int x = 1; x < 3; x++) {
            adaptadorViewPager.agregarFragmento(new FragmentFormularioDeAvio(), String.valueOf(x));
        }
        viewPager.setAdapter(adaptadorViewPager);
    }

    class AdaptadorViewPager extends FragmentPagerAdapter {
        private final List<Fragment> listaDeFragmentos = new ArrayList<>();
        private final List<String> listaDeTitulosDeFragmentos = new ArrayList<>();

        AdaptadorViewPager(FragmentManager manager) {
            super(manager);
        }

        @Override
        public Fragment getItem(int position) {
            return listaDeFragmentos.get(position);
        }

        @Override
        public int getCount() {
            return listaDeFragmentos.size();
        }

        void agregarFragmento(Fragment fragment, String title) {
            listaDeFragmentos.add(fragment);
            listaDeTitulosDeFragmentos.add(title);
        }

        // Si es el título de la última pestaña, regresamos null, lo
        // cual regresará el icono únicamente
        @Override
        public CharSequence getPageTitle(int position) {
            if (position >= getCount() - 1) return null;
            return listaDeTitulosDeFragmentos.get(position);
        }
    }
}

El código se explica por sí mismo (o eso espero) y los comentarios ayudan un poco.

Así es como se consigue tener Tabs dinámicas en Android, ya dentro del fragmento podemos hacer cualquier cosa.

También recuerda que le puedes poner iconos a todas las pestañas, en este caso lo hice así para que se vea más dinámico. De igual forma, no es obligatorio que el título sea el número de pestaña.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.