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.
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
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.