Interfaz de Usuario en Android: Navigation Drawer (NavigationView)

En este nuevo artículo del curso vamos a tratar otro de los componentes relacionados con la capa de presentación de nuestras aplicaciones, el menú lateral deslizante, o Navigation Drawer. Este menú es el que aparece en muchas aplicaciones al deslizar el dedo desde el borde izquierdo de la pantalla hacia el lado opuesto (también puede aparecer en el lado derecho, pero es menos frecuente).

El navigation drawer está disponible como parte de la librería de compatibilidad android-support-v4. Si estamos trabajando con Android Studio, en la mayoría de los casos no tendremos que añadir esta librería al proyecto de forma explícita ya que muy probablemente tengamos ya añadida la librería appcompat-v7 (se incluye por defecto en los proyectos de Android Studio), que depende de la primera.

En este artículo vamos a crear un menú lateral que cumpla todo lo posible las directrices marcadas por las nuevas guías de diseño de Material Design. Dicha guía establece que el menú de navegación lateral debe pasar por encima de la action bar y por debajo de la status bar (que será translúcida en Android 5.0 y posteriores).

nav-drawer-material

Veremos cómo conseguir este efecto para versiones de Android a partir de la 5.0 Lollipop (API 21), mientras que en versiones anteriores la status bar permanecerá de color negro.

Vamos a empezar a crear nuestra aplicación, y comenzaremos como siempre creando la interfaz de usuario. Para añadir el navigation drawer a una actividad debemos hacer que el elemento raíz del layout XML sea del tipo <android.support.v4.widget.DrawerLayout>. Y dentro de este elemento colocaremos únicamente 2 componentes principales (en el orden indicado):

  1. El layout real de la actividad.
  2. El layout del menú lateral, que entre otras cosas hará las veces de contenedor de las distintas opciones del menú lateral.

El primero de estos elementos lo añadiremos por ahora en forma de <include> y después volveremos sobre él. Para el segundo vamos a utilizar otro de los nuevos componentes incluidos con la nueva librería de diseño de Android (Design Support Library). El componente en cuestión es el llamado NavigationView, que nos ayudará bastante en la construcción del layout del menú lateral.

En mi caso de ejemplo quedaría como sigue (/res/layout/activity_main.xml):

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <!-- Layout real de la actividad -->
    <include layout="@layout/content_layout" />

    <!-- Layout del menú lateral (Navigation View) -->
    <android.support.design.widget.NavigationView
        android:id="@+id/navview"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:layout_gravity="start"
        app:headerLayout="@layout/header_navview"
        app:menu="@menu/menu_navview" />

</android.support.v4.widget.DrawerLayout>

Varios detalles a destacar en el código anterior. En cuanto al DrawerLayout es importante asignar a true la propiedad android:fitsSystemWindows, que ayudará a conseguir el efecto indicado de deslizamiento del menú por debajo de la status bar.

Respecto al NavigationView, vemos como también asignamos su propiedad fitsSystemWindows de forma análoga al DrawerLayout. Posteriormente asignamos las propiedades quizá más relevantes del nuevo componente:

  • La primera de ellas, android:layout_gravity, determina el lado de la pantalla por el que aparecerá el menú deslizante (“start” para que aparezca por la izquierda, o “end” por la derecha).
  • Con app:headerLayout (opcional) asignamos al menú lateral el layout XML de su cabecera, es decir, de la zona que queda por encima de la lista de opciones del menú.
  • Por último, con app:menu, indicamos el recurso de menú que mostraremos en el navigation drawer. El componente NavigationView utiliza el sistema de menús habitual de Android, por lo que este menú podemos definirlo de forma análoga a como ya lo hicimos por ejemplo en el artículo dedicado a la action bar para definir el menú de overflow, aunque más adelante mostraremos alguna peculiaridad.

Veamos a continuación la definición del layout XML utilizado como cabecera del navigation drawer (/res/layout/header_navview.xml). En la cabecera del menú lateral suele incluirse en muchas aplicaciones información sobre el usuario logueado, y opciones para cambiar de usuario si esto fuera posible. En mi caso, para no complicar el ejemplo, voy a utilizar tan sólo una imagen de fondo y una etiqueta de texto, aunque aclarar que este layout puede ser todo lo complejo que sea necesario.

<FrameLayout
    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="200dp"
        android:src="@drawable/navheader"
        android:scaleType="centerCrop" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/usuario"
        android:textAppearance="@style/TextAppearance.AppCompat.Large.Inverse"
        android:textStyle="bold"
        android:layout_gravity="bottom"
        android:layout_marginBottom="10dp"
        android:layout_marginLeft="10dp" />
</FrameLayout>

Respecto al menú a utilizar, ya dijimos que se definirá utilizando la sintaxis habitual de los recursos de tipo menú. En mi caso añadiré tres secciones principales y dos opciones adicionales:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/menu_seccion_1"
            android:icon="@drawable/ic_menu"
            android:title="@string/seccion_1"/>
        <item
            android:id="@+id/menu_seccion_2"
            android:icon="@drawable/ic_menu"
            android:title="@string/seccion_2"/>
        <item
            android:id="@+id/menu_seccion_3"
            android:icon="@drawable/ic_menu"
            android:title="@string/seccion_3"/>
    </group>

    <item
        android:id="@+id/navigation_subheader"
        android:title="@string/otras_opciones">
        <menu>
            <item
                android:id="@+id/menu_opcion_1"
                android:icon="@drawable/ic_menu"
                android:title="@string/opcion_1"/>
            <item
                android:id="@+id/menu_opcion_2"
                android:icon="@drawable/ic_menu"
                android:title="@string/opcion_2"/>
        </menu>
    </item>
</menu>

Comentemos algunos detalles de la definición anterior. En primer lugar vemos que las tres secciones principales se engloban en un elemento <group> al que hemos asignado su propiedad checkableBehavior con valor “single“. Con esto indicamos que sólo pueda seleccionarse una de estas tres opciones al mismo tiempo (más tarde veremos como resaltar la opción seleccionada). A continuación se añaden dos opciones más dentro de un submenú al que asignamos su título con la propiedad android:title. Este título del submenú aparecerá en forma de cabecera de sección dentro del menú, incluyendo incluso una linea de división tras las tres opciones anteriores. Adicionalmente, en todas las opciones de menú indicamos su id (android:id), su icono (android:icon) y su título (android:title). En mi caso he utilizado por simplicidad el mismo icono en todas las opciones, pero por supuesto pueden ser distintos.

En una imagen veremos mejor el resultado:

menu_ejemplo

Como nota importante indicar que las opciones incluidas dentro de un submenú no es posible resaltarlas en la interfaz como sí ocurre con las opciones anteriores incluidas dentro del elemento group. Es posible que en futuras versiones de la librería de diseño se habilite esta posibilidad.

Veamos a continuación el layout de la actividad principal. Para este ejemplo utilizaré un layout muy similar a los ya mostados en los artículos anteriores sobre la action bar. Contendrá tan sólo un Toolbar, que estableceremos como action bar en el onCreate() de la actividad, y un FrameLayout que nos servirá como contenedor de los fragment que contendrán cada sección del menú lateral. Dicho de otra forma, cada sección principal de la aplicación la implementaremos mediante un fragment independiente, y al pulsar cada opción del menú lateral, instanciaremos el fragment de su tipo correspondiente y lo colocaremos en el lugar del FrameLayout indicado. Veamos cómo quedaría (/res/layout/content_layout.xml):

<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"
    android:clickable="true"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <!-- Toolbar -->
    <android.support.v7.widget.Toolbar
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/appbar"
        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" />

    <!-- Resto de la interfaz de usuario -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Para que la toolbar se visualice correctamente habrá que definir por supuesto el tema de la aplicación, los colores principales, … tal y como ya vimos en el artículo sobre el componente Toolbar.

Un último detalle de la configuración XML de la aplicación, y que nos servirá para terminar de conseguir el efecto deseado. Para versiones de Android 5.0 o superior (API >= 21) debemos añadir a la definición del tema algunos atributos adicionales, para lo que crearemos un fichero styles.xml específico de dicha versión (/res/values-21/styles.xml) con las siguientes definiciones:

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/color_primary</item>
        <item name="colorPrimaryDark">@color/color_primary_dark</item>
        <item name="colorAccent">@color/color_accent</item>

        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>

Definida la interfaz XML, nos centramos ya en la parte java de la actividad. En primer lugar vamos a crear los fragments que mostraremos al seleccionar cada una de las tres opciones del menú de navegación. En este paso no nos vamos a complicar ya que no es el objetivo de este artículo. Voy a crear un fragment por cada opción, que contenga tan sólo una etiqueta de texto indicando la opción a la que pertenece. Obviamente en la práctica esto no será tan simple y habrá que definir cada fragment para que se ajuste a las necesidades de la aplicación.

Como ejemplo muestro el layout XML y la implementación java de uno de los layout. Primero el layout (fragment_fragment1.xml) :

<FrameLayout
    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"
    tools:context="net.sgoliver.android.navigationdrawer.Fragment1">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/fragment1" />

</FrameLayout>

Y su clase java asociada (Fragment1.java), que se limitará a inflar el layout anterior:

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

public class Fragment1 extends Fragment {

    public Fragment1() {
        // 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_fragment1, container, false);
    }
}

Los dos fragments restantes serán completamente análogos al mostrado.

Lo siguiente será implementar la lógica necesaria para responder a los eventos del menú de forma que cambiemos de fragment al pulsar cada opción. Esto lo haremos implementando el evento onNavigationItemSelected() del control NavigationView del menú lateral, lógica que añadiremos al final del método onCreate() de nuestra actividad principal.

drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
navView = (NavigationView)findViewById(R.id.navview);

navView.setNavigationItemSelectedListener(
    new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem menuItem) {

            boolean fragmentTransaction = false;
            Fragment fragment = null;

            switch (menuItem.getItemId()) {
                case R.id.menu_seccion_1:
                    fragment = new Fragment1();
                    fragmentTransaction = true;
                    break;
                case R.id.menu_seccion_2:
                    fragment = new Fragment2();
                    fragmentTransaction = true;
                    break;
                case R.id.menu_seccion_3:
                    fragment = new Fragment3();
                    fragmentTransaction = true;
                    break;
                case R.id.menu_opcion_1:
                    Log.i("NavigationView", "Pulsada opción 1");
                    break;
                case R.id.menu_opcion_2:
                    Log.i("NavigationView", "Pulsada opción 2");
                    break;
            }

            if(fragmentTransaction) {
                getSupportFragmentManager().beginTransaction()
                    .replace(R.id.content_frame, fragment)
                    .commit();

                menuItem.setChecked(true);
                getSupportActionBar().setTitle(menuItem.getTitle());
            }

            drawerLayout.closeDrawers();

            return true;
        }
    });

Comentemos un poco el código anterior.

Para las tres secciones principales lo que hacemos en primer lugar es crear el nuevo fragment a mostrar dependiendo de la opción pulsada en el menú de navegación, que nos llega como parámetro (menuItem) del evento onNavigationItemSelectedEn el siguiente paso hacemos uso del Fragment Manager con getSupportFragmentManager() para sustituir el contenido del FrameLayout que definimos en el layout de la actividad principal por el nuevo fragment creado. Posteriormente marcamos como seleccionada la opción pulsada del menú mediante el método setChecked() y actualizamos el título de la action bar por el de la opción seleccionada mediante setTitle().  

Por su parte, para las dos opciones finales del menú podemos realizar por ejemplo cualquier otra acción que no implique cambio de fragment (como abrir una actividad independiente para mostrar una ayuda o las opciones de la aplicación). En mi caso de ejemplo me limito a mostrar un mensaje de log llamando a Log.i().

Por último, y en cualquier caso, cerramos el menú llamando al método closeDrawers() del DrawerLayout.

Bien, pues ya tenemos la funcionalidad básica implementada, y sólo nos quedaría ajustar algunos detalles más para finalizar el trabajo. Los más importantes: deberíamos mostrar un indicador en la action bar que evidencie al usuario la existencia del menú lateral y deberíamos además permitir al usuario abrirlo haciendo click en dicho icono (además del gesto de deslizar desde el borde izquierdo hacia la derecha). Hasta la llegada de la nueva librería de diseño, esto se realizaba haciendo uso del componente ActionBarDrawerToggle, pero con la nueva librería el proceso se ha simplificado bastante y ya no es necesario la utilización de dicha clase.

Debemos primero incluir en nuestra aplicación el icono habitual que indica la existencia de un navigation drawer (hamburguer icon). Puedes crear tu propio icono desde cero, generarlo y descargarlo utilizando alguna herramienta online como Android Asset Studio, o simplemente descargar desde GitHub el que yo he utilizado como ejemplo para este artículo (se llama ic_nav_menu, y debes descargar el icono en todas sus resoluciones, desde las carpetas de drawables mdpi, hdpi, xhdpi, xxhdpi y xxxhdpi).

Una vez tenemos el icono incluido al proyecto, podemos utilizarlo como indicador del menú lateral llamando al método setHomeAsUpIndicator() desde el onCreate() de la actividad principal. Adicionalmente habilitaremos esta funcionalidad llamando también a setDisplayHomeAsUpEnabled().

@Override
protected void onCreate(Bundle savedInstanceState) {

    //...

    appbar = (Toolbar)findViewById(R.id.appbar);
    setSupportActionBar(appbar);

    getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_nav_menu);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    //...
}

Adicionalmente, tendremos que capturar en el método onOptionsItemSelected() de la actividad principal si se ha pulsado el icono. Para ello, evaluaremos si la opción de menú pulsada es android.R.id.home, en cuyo caso abriremos el menú lateral llamando al método openDrawer() del DrawerLayout:

@Override
public boolean onOptionsItemSelected(MenuItem item) {

    switch(item.getItemId()) {
        case android.R.id.home:
            drawerLayout.openDrawer(GravityCompat.START);
            return true;
        //...
    }

    return super.onOptionsItemSelected(item);
}

Llegados aquí, podemos ejecutar el proyecto y ver si todo funciona correctamente. Verificaremos que el menú se abra, que contiene las opciones indicadas y que al pulsar sobre ellas aparece en pantalla el contenido asociado. Verificaremos además que el título de la action bar se va actualizando según el estado del menú y la opción seleccionada.

Si lo ejecutamos sobre Android 5.x lo veremos como se muestra en la imagen anterior de este artículo (el menú se desliza por debajo de la status bar translúcida), y sobre Android 4.x se vería de forma casi idéntica a excepción de la barra de estado que sería negra:

menu_ejemplo_android4

Espero que estos últimos artículos os hayan servido para aprender a construir aplicaciones utilizando los componentes de diseño más representativos de la plataforma Android (la Action Bar y el Navigation Drawer).

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.