Diseño Android: Toolbar y Pestañas con Material Design

android

Uno de los componentes gráficos definidos por Material Design que más rápidamente han sido adoptados en Android es el nuevo estilo de pestañas integradas en la barra de acciones. Podemos encontrar una infinidad de ejemplos de su implementación, por ejemplo Google Play:

google play tabs

Los widgets necesarios para implementar este patrón de diseño los tenemos disponibles en la Support Library y serán utilizados en el presente tutorial para construir una interfaz de pestañas estilo Material Design. Para ello me voy a apoyar en lo expuesto en los tutoriales “Diseño Android: ActionBar con Toolbar” y “Diseño Android: ViewPager” por lo que remito al lector a los mismos para profundizar en estos elementos si no los ha utilizado anteriormente.

Veamos cómo construir nuestra interfaz de pestañas paso a paso.

  1. Incluimos los dos módulos de la Support Library que vamos a utilizar.

    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.android.support:design:23.2.1'
    

    Para más información acerca de cómo incluir librerías en Eclipse ADT y Android Studio, consultar este tutorial.

  2. Nuestra interfaz de pestañas de ejemplo estará definida en el fichero activity_main.xml. Crearemos un layout con los siguientes elementos:
    • AppBarLayout: este layout es el “bloque” que incluye la Toolbar y, debajo de esta, las pestañas con TabLayout. AppBarLayout se encargará de que ambos elementos tengan el estilo adecuado y visualmente se muestren “integrados”.
    • ViewPager: es el paginador que muestra el contenido de cada pestaña. En este tutorial permitiremos la navegación entre páginas deslizándolas y pulsando en las pestañas (comportamiento por defecto).

    La pantalla queda tal y como sigue:

    <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.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/ThemeOverlay.AppCompat.Dark">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                tools:ignore="UnusedAttribute"/>
    
            <android.support.design.widget.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:tabGravity="fill"
                app:tabIndicatorColor="@android:color/white"/>
        </android.support.design.widget.AppBarLayout>
    
        <android.support.v4.view.ViewPager
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/darker_gray"/>
    
    
    </LinearLayout>
    
  3. Con respecto a los estilos, se ha aplicado al AppBarLayout el tema “oscuro” ThemeOverlay.AppCompat.Dark que hará que los textos de la Toolbar y las pestañas se muestren en blanco. En la TabLayout se ha establecido el color del indicador de pestaña activo a blanco con el atributo tabIndicatorColor, y con el atributo app:tabGravity=”fill” forzamos a que las pestañas ocupen todo el espacio disponible en pantalla suponiendo, claro está, que es el comportamiento que queremos.
    El estilo general para la aplicación, definido en el styles.xml, es el siguiente:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="colorPrimary">@color/primary</item>
            <item name="colorPrimaryDark">@color/primaryDark</item>
            <item name="popupTheme">@style/Theme.AppCompat.Light</item>
        </style>
    
    </resources>
    
  4. Ahora vamos con la implementación de MainActivity que debe heredar de AppCompatActivity. Las tareas a realizar son:
    • Implementar un Adapter, en el ejemplo es una clase interna de la Activity, para el ViewPager heredando de PageAdapter que se encargará de mostrar en pantalla el layout correspondiente a cada página. Para este ejemplo recuperamos el código y los layout del tutorial “Diseño Android: ViewPager”. Para el presente tutorial será necesario sobreescribir el método getPageTitle para que devuelve el nombre de la pestaña que queremos para cada página (posición) del paginador.

        class MainPageAdapter extends PagerAdapter {
      
          private LinearLayout page1;
          private LinearLayout page2;
          private ListView page3;
          private LinearLayout page4;
          private final int[] titles = {R.string.page1, R.string.page2, R.string.page3, R.string.page4};
      
          @Override
          public int getCount() {
            return 4;
          }
      
          @Override
          public CharSequence getPageTitle(int position) {
              return getString(titles[position]);
          }
      
          @Override
          public Object instantiateItem(ViewGroup collection, int position) {
            View page;
            switch (position) {
              case 0:
                if (page1 == null) {
                  page1 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_one, collection, false);
                }
                page = page1;
                break;
              case 1:
                if (page2 == null) {
                  page2 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_two, collection, false);
                }
                page = page2;
                break;
              case 2:
                if (page3 == null) {
                  page3 = (ListView) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_three, collection, false);
                  initListView();
                }
                page = page3;
                break;
              default:
                if (page4 == null) {
                  page4 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_four, collection, false);
                }
                page = page4;
                break;
            }
      
            collection.addView(page, 0);
      
            return page;
          }
      
          @Override
          public boolean isViewFromObject(View view, Object object) {
            return view == object;
          }
      
          @Override
          public void destroyItem(ViewGroup collection, int position, Object view) {
            collection.removeView((View) view);
          }
      
          private void initListView() {
            String[] items = new String[50];
            for (int i = 0; i < 50; i++) {
              items[i] = "Item " + i;
            }
            page3.setAdapter(new ArrayAdapter<String>(MainActivity.this, R.layout.textview, items));
            page3.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      
              @Override
              public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(MainActivity.this, (String) parent.getItemAtPosition(position), Toast
                    .LENGTH_SHORT).show();
              }
            });
      
          }
        }
      
    • En el onCreate establecemos la Toolbar como ActionBar
       Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
       setSupportActionBar(toolbar);
      
    • se asigna al Viewpager el PageAdapter
       ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
       viewPager.setAdapter(new MainPageAdapter());
       setSupportActionBar(toolbar);
      
    • y por último se asigna al TabLayout el ViewPager y configuramos el modo de las pestañas: MODE_FIXED si queremos que las pestañas se muestren todas en pantalla a la vez, o MODE_SCROLLABLE para que sólo se muestren las que quepan en pantalla y se pueda hacer scroll para navegar por las mismas. Normalmente usaremos el primer modo si no tenemos más de cuatro pestañas. Este valor también se puede establecer en el XML con el atributo app:tabMode=”fixed/scollable”.
      TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
      tabLayout.setTabMode(TabLayout.MODE_FIXED);
      tabLayout.setupWithViewPager(viewPager);
      
    • La clase al completo queda como sigue (se incluye un menú con el elemento “about” para que el ejemplo sea más completo):

      package com.danielme.mdtabs;
      
      import android.content.Intent;
      import android.net.Uri;
      import android.os.Bundle;
      import android.support.design.widget.TabLayout;
      import android.support.v4.view.PagerAdapter;
      import android.support.v4.view.ViewPager;
      import android.support.v7.app.AppCompatActivity;
      import android.support.v7.widget.Toolbar;
      import android.view.LayoutInflater;
      import android.view.Menu;
      import android.view.MenuInflater;
      import android.view.MenuItem;
      import android.view.View;
      import android.view.ViewGroup;
      import android.widget.AdapterView;
      import android.widget.ArrayAdapter;
      import android.widget.LinearLayout;
      import android.widget.ListView;
      import android.widget.Toast;
      
      
      public class MainActivity extends AppCompatActivity {
      
        @Override
        protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
      
          Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
          setSupportActionBar(toolbar);
      
          ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
          viewPager.setAdapter(new MainPageAdapter());
      
          TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
          tabLayout.setTabMode(TabLayout.MODE_FIXED);
          tabLayout.setupWithViewPager(viewPager);
      
        }
      
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
          MenuInflater inflater = getMenuInflater();
          inflater.inflate(R.menu.main_activity_menu, menu);
          return super.onCreateOptionsMenu(menu);
        }
      
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
          switch (item.getItemId()) {
            case R.id.action_about:
              startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://danielme" +
                  ".com/2016/03/26/diseno-android-tutorial-pestanas-con-material-design")));
              break;
            default:
              return super.onOptionsItemSelected(item);
          }
          return true;
        }
      
      
        class MainPageAdapter extends PagerAdapter {
      
          private LinearLayout page1;
          private LinearLayout page2;
          private ListView page3;
          private LinearLayout page4;
          private final int[] titles = {R.string.page1, R.string.page2, R.string.page3, R.string.page4};
      
          @Override
          public int getCount() {
            return 4;
          }
      
          @Override
          public CharSequence getPageTitle(int position) {
              return getString(titles[position]);
          }
      
          @Override
          public Object instantiateItem(ViewGroup collection, int position) {
            View page;
            switch (position) {
              case 0:
                if (page1 == null) {
                  page1 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_one, collection, false);
                }
                page = page1;
                break;
              case 1:
                if (page2 == null) {
                  page2 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_two, collection, false);
                }
                page = page2;
                break;
              case 2:
                if (page3 == null) {
                  page3 = (ListView) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_three, collection, false);
                  initListView();
                }
                page = page3;
                break;
              default:
                if (page4 == null) {
                  page4 = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout
                      .page_four, collection, false);
                }
                page = page4;
                break;
            }
      
            collection.addView(page, 0);
      
            return page;
          }
      
          @Override
          public boolean isViewFromObject(View view, Object object) {
            return view == object;
          }
      
          @Override
          public void destroyItem(ViewGroup collection, int position, Object view) {
            collection.removeView((View) view);
          }
      
          private void initListView() {
            String[] items = new String[50];
            for (int i = 0; i < 50; i++) {
              items[i] = "Item " + i;
            }
            page3.setAdapter(new ArrayAdapter<String>(MainActivity.this, R.layout.textview, items));
            page3.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      
              @Override
              public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(MainActivity.this, (String) parent.getItemAtPosition(position), Toast
                    .LENGTH_SHORT).show();
              }
            });
      
          }
        }
      
      }
      
      

    El siguiente video muestra el resultado en un emulador con Marshmallow.


    Iconos

    TabLayout permite la utilización de iconos tanto con texto como sin el. Para añadir un icono a las pestañas de nuestros icono simpñemente hay que utilizar el método setIcon para cada pestaña. El código quedaría tal que así:

    tabLayout.getTabAt(0).setIcon(R.mipmap.tab1);
    tabLayout.getTabAt(1).setIcon(R.mipmap.tab2);
    tabLayout.getTabAt(2).setIcon(R.mipmap.tab3);
    tabLayout.getTabAt(3).setIcon(R.mipmap.tab4);
    

    Sin embargo, esto no es suficiente ya que generalmente también vamos a necesitar que el icono sea distinto en función de su estado (seleccionado/no seleccionado) y que por tanto tenga el mismo estilo que el texto. Para conseguir esto no es necesario tener dos imágenes para cada icono ya que programáticamente se puede modificar la transparencia alpha del icono al cambiarse de pestaña.

    Lo que haremos será detectar el cambio de pestaña implementando ViewPager.OnPageChangeListener. Modificaremos la propiedad ALPHA de los distintos iconos.

    public class MainActivity extends AppCompatActivity {
    
      private static final int ALPHA_SELECTED = 255;
      private static final int ALPHA_UNSELECTED = 128;
      private static final int NUM_TABS = 4;
    
      private TabLayout tabLayout;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
        viewPager.setAdapter(new MainPageAdapter());
    
        tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.setTabMode(TabLayout.MODE_FIXED);
        tabLayout.setupWithViewPager(viewPager);
        tabLayout.getTabAt(0).setIcon(R.mipmap.tab1);
        tabLayout.getTabAt(1).setIcon(R.mipmap.tab2);
        tabLayout.getTabAt(2).setIcon(R.mipmap.tab3);
        tabLayout.getTabAt(3).setIcon(R.mipmap.tab4);
    
        selectIcon(0);
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
          @Override
          public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            //nothing here
          }
    
          @Override
          public void onPageSelected(int position) {
            selectIcon(position);
          }
    
          @Override
          public void onPageScrollStateChanged(int state) {
            //nothing here
          }
        });
    
      }
    
      private void selectIcon(int position) {
        for (int i = 0; i < NUM_TABS; i++) {
          if (i == position) {
            tabLayout.getTabAt(i).getIcon().setAlpha(ALPHA_SELECTED);
          } else {
            tabLayout.getTabAt(i).getIcon().setAlpha(ALPHA_UNSELECTED);
          }
        }
    
      }
    

    Para que sólo se muestre el icono sin texto tendríamos que eliminar el método getPageTitle del PageAdapter.

    El siguiente video muestra el resultado final del proyecto de ejemplo en un emulador con Marshmallow.


    Código de ejemplo

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

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: