Interfaz de Usuario en Android: Navigation Drawer

En el artículo anterior hablamos sobre una de las últimas novedades del SDK de Android, la nueva versión de la Action Bar incluida en la librería de compatibilidad android-support. En este nuevo artículo vamos a tratar otra de las novedades presentadas en el Google I/O de este año relacionadas la capa de presentación de nuestras aplicaciones, el menú lateral deslizante, o dicho de una forma mucho más moderna y elegante: el Navigation Drawer.

Este tipo de menús de navegación ya llevaban tiempo utilizándose y proliferando por el market en numerosas aplicaciones, pero igual que pasaba con la action bar en versiones de Android anteriores a la 3.0, se hacía gracias a librerías externas que implementaban este componente, o a diversas implementaciones ad-hoc, todas ellas distintas e incoherentes entre sí, tanto en diseño como en funcionalidad. Google ha querido acabar con esto aportando su propia implementación de este componente y definiendo su comportamiento en las guías de diseño de la plataforma.

En este caso, en la documentación oficial de Android (en inglés, por supuesto) está bastante bien explicado cómo incluir este elemento en nuestras aplicaciones, pero aún así voy a detallarlo paso a paso en este artículo intercalando algunas notas que no aparecen en la documentación y que pueden evitaros algunas sorpresas.

El navigation drawer está disponible como parte de la librería de compatibilidad android-support. Para poder utilizarlo en nuestras aplicaciones tendremos que asegurarnos que tenemos incluida en nuestro proyecto la librería android-support-v4.jar (debe ser la revisión 18 o superior, podemos comprobar cuál tenemos instalada en el SDK Manager). Si tenemos instalada una versión reciente del plugin de Android en Eclipse, al crear un nuevo proyecto se añade directamente esta librería a la carpeta /lib.

Como ejemplo para este artículo, partiré del código ya construído en el artículo anterior sobre la Action Bar Compat, ya que es interesante ver cómo interactúan ambos elementos en la misma aplicación y cómo podemos hacerlo compatible con todas las versiones de Android.

Comprobado que tenemos incluida la librería android-suport, ya podemos comenzar 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 (en el orden indicado):

  1. Un FrameLayout, que nos servirá más tarde como contenedor de la interfaz real de la actividad, que crearemos a base de fragments. Ajustaremos el ancho y el alto para que ocupe todo el espacio disponible (match_parent).
  2. Un ListView, que hará las veces de contenedor de las distintas opciones del menú lateral. En este caso ajustaremos el alto para ocupar todo el espacio, y el ancho a un tamaño no superior a 320dp, de forma que cuando esté el menú abierto no oculte totalmente el contenido principal de la pantalla.

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"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <!-- Contenido Principal -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Menú Lateral -->
    <ListView
        android:id="@+id/left_drawer"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#111"
        android:choiceMode="singleChoice" />

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

Definida la interfaz XML, nos centramos ya en la parte java de la actividad. Lo primero que haremos será añadir al menú (recordemos que se trata realmente de un ListView) las opciones que queremos que aparezcan disponibles. Para no complicar el ejemplo las añadiré directamente desde un array java convencional (como alternativa, en la documentación de Google tenéis un ejemplo de cómo cargar las opciones desde un XML). La forma de incluir estas opciones a la lista es mediante un adaptador normal, igual que hemos comentado en ocasiones anteriores. Lo haremos por ejemplo dentro del método onCreate() de la actividad:

//...
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;

public class MainActivity extends ActionBarActivity {

	private String[] opcionesMenu;
	private DrawerLayout drawerLayout;
	private ListView drawerList;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		opcionesMenu = new String[] {"Opción 1", "Opción 2", "Opción 3"};
		drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
		drawerList = (ListView) findViewById(R.id.left_drawer);

		drawerList.setAdapter(new ArrayAdapter<String>(
        		getSupportActionBar().getThemedContext(),
			android.R.layout.simple_list_item_1, opcionesMenu));
	}

	//...
}

Como podéis ver en el código anterior, el contexto pasado como parámetro al adaptador lo hemos obtenido mediante una llamada al método getThemedContext() de la action bar (relevante también el haber usado getSupportActionBar() en vez de getActionBar() por estar utilizando la versión de la action bar incluida en la librería de compatibilidad). Por decirlo una forma sencilla, esto nos asegurará que los elementos de la lista se muestren acordes al estilo de la action bar. Además, vemos que como layout de los elementos de la lista he utilizado el estandar android.R.layout.simple_list_item_1. Esto nos asegura compatibilidad con la mayoría de versiones de Android sin complicarnos mucho, aunque tiene algunos problemas. Por ejemplo, la opción seleccionada no se mantendrá resaltada en el menú una vez se cierre y se vuelva a abrir. Para conseguir esto en Android 4 podemos simplemente usar el layout android.R.layout.simple_list_item_activated_1, aunque debemos tener en cuenta que la aplicación generará un error si se ejecuta sobre versiones anteriores. Para evitar este problema de compatibilidad tenemos varias opciones:

  • Utilizar un layout u otro dependiendo de la versión de Android sobre la que se está ejecutando la aplicación, asumiendo así que la opción seleccionada se mantendrá resaltada en Android 4 pero no en versiones anteriores. Sería algo así:
drawerList.setAdapter(new ArrayAdapter<String>(getSupportActionBar().getThemedContext(),
    (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) ?
    android.R.layout.simple_list_item_activated_1 :
    android.R.layout.simple_list_item_1, opcionesMenu));
  • Utilizar un layout alternativo que mantenga la opción reslatada aunque no se muestre igual que en Android 4. Por ejemplo si usamos el layout simple_list_item_checked en Android 2.x la opción se mantendrá resaltada con una check a la derecha de su nombre. En este caso quedaría así:
drawerList.setAdapter(new ArrayAdapter<String>(getSupportActionBar().getThemedContext(),
    (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) ?
    android.R.layout.simple_list_item_activated_1 :
    android.R.layout.simple_list_item_checked, opcionesMenu));
  • Implementar un adaptador personalizado para establecer manualmente el estilo de la opción seleccionada de la lista. No entraré en los detalles de esta opción porque se sale un poco del objetivo de este artículo, pero tienes información sobre cómo crear adaptadores personalizados en artículos anteriores.

Para no complicar nuestro caso de ejemplo vamos a dejarlo tal como está, por lo que la opción seleccionada no quedará resaltada en el menú.

A continuación vamos a crear los fragments que mostraremos al seleccionar cada una de las tres opciones del menú de navegación. Y 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_1.xml) :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/TxtDetalle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/fragment1" />

</LinearLayout>

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

package net.sgoliver.android.navdrawer;

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 {

	@Override
	public View onCreateView(
		LayoutInflater inflater, ViewGroup container,
		Bundle savedInstanceState) {

		return inflater.inflate(R.layout.fragment_1, container, false);
	}
}

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

Ya tenemos listo el menú y los fragments asociados a cada opción. 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 onItemClick del control ListView del menú, lógica que añadiremos al final del método onCreate() de nuestra actividad principal.

@Override
protected void onCreate(Bundle savedInstanceState) {

	//...

	drawerList.setOnItemClickListener(new OnItemClickListener() {
		@Override
		public void onItemClick(AdapterView parent, View view,
				int position, long id) {

			Fragment fragment = null;

			switch (position) {
				case 1:
					fragment = new Fragment1();
					break;
				case 2:
					fragment = new Fragment2();
					break;
				case 3:
					fragment = new Fragment3();
					break;
			}

			FragmentManager fragmentManager =
				getSupportFragmentManager();

			fragmentManager.beginTransaction()
				.replace(R.id.content_frame, fragment)
				.commit();

			drawerList.setItemChecked(position, true);

			tituloSeccion = opcionesMenu[position];
			getSupportActionBar().setTitle(tituloSeccion);

			drawerLayout.closeDrawer(drawerList);
		}
	});
}

Comentemos un poco el código anterior. En primer lugar lo que hacemos 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 (position) del evento onItemClick. En el siguiente paso hacemos uso del Fragment Manager (con getSupportFragmentManager() para hacer uso una vez más de la librería de compatibilidad) 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 de la lista mediante el método setItemChecked(), actualizamos el título de la action bar por el de la opción seleccionada, y por último cerramos el menú llamando a closeDrawer().

Bien, pues ya tenemos la funcionalidad básica implementada. Ahora nos quedaría ajustar algunos detalles para respetar las pautas definidas en la guía de diseño del componente Navigation Drawer. Según las recomendaciones de esta esta guía deberíamos mostrar un indicador en la action bar que evidencia al usuario la existencia del menú lateral, deberíamos además permitir al usuario abrirlo haciendo click en el icono de la aplicación (además del gesto de deslizar desde el borde izquierdo hacia la derecha), y adicionalmente cuando esté abierto el menú deberíamos actualizar el título de la action bar y ocultar aquellas acciones relacionadas exclusivamente con el contenido principal actual (semioculto por el menú). La mayoría de estas tareas las vamos a realizar ayudándonos de una clase auxiliar llamada ActionBarDrawerToggle. Vayamos por partes.

Vamos a comenzar creando un objeto de esta clase ActionBarDrawerToggle y asociándolo a nuestro navigation drawer mediante el método setDrawerListener() para, entre otras cosas, responder a través de él a los eventos de apertura y cierre del menú. Para esto último sobrescribiremos sus métodos onDrawerOpened() y onDrawerClosed(). Todo esto lo haremos también dentro del método onCreate() de nuestra clase principal. Veamos el código y a continuación lo comentaremos:

@Override
protected void onCreate(Bundle savedInstanceState) {

	//...

	tituloApp = getTitle();

	drawerToggle = new ActionBarDrawerToggle(this,
		drawerLayout,
		R.drawable.ic_navigation_drawer,
		R.string.drawer_open,
		R.string.drawer_close) {

		public void onDrawerClosed(View view) {
			getSupportActionBar().setTitle(tituloSeccion);
			ActivityCompat.invalidateOptionsMenu(MainActivity.this);
		}

		public void onDrawerOpened(View drawerView) {
			getSupportActionBar().setTitle(tituloApp);
			ActivityCompat.invalidateOptionsMenu(MainActivity.this);
		}
	};

	drawerLayout.setDrawerListener(drawerToggle);
}

En primer lugar vemos que el constructor de la clase ActionBarDrawerToggle recibe 5 parámetros: como casi siempre el contexto actual, una referencia al navigation drawer, el ID del icono a utilizar como indicador del navigation drawer, y los ID de dos cadenas de caracteres que se utilizar a efectos de accesibilidad de la aplicación (en mi caso las defino simplemente con los valores “Menú Abierto” y “Menú Cerrado”). Para obtener un icono apropiado para el indicador del navigation drawer podéis utilizar la utilidad Navigation Drawer Indicator Generator del Android Asset Studio, que os permitirá generar y descagar el icono de forma que tan sólo tenéis que copiarlo a las carpetas /res/drawable-xxx de vuestro proyecto.

A continuación se implementan los eventos de apertura y cierre del menú lateral. Lo único que haremos en estos eventos será actualizar el título de la action bar para mostrar el título de la aplicación (cuando el menú está abierto) o el título de la opción seleccionada actualmente (cuando el menú está cerrado). Además, al final de cada uno de ellos hacemos una llamada a invalidateOptionsMenu() para provocar que se ejecute el evento onPrepareOptionsMenu() de la actividad, donde nos ocuparemos de ocultar las acciones de la action bar que no apliquen cuando el menú lateral esté abierto. Importante llamar a este método haciendo uso de nuevo de su alternativa incluida en la librería de compatibilidad, como método de la clase ActivityCompat, ya que el método invalidateOptionsMenu() apareció con la API 11 (Android 3.0) y no funcionaría en versiones anteriores.

En nuestro caso de ejemplo ocultaremos la acción de buscar:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {

	boolean menuAbierto = drawerLayout.isDrawerOpen(drawerList);

	if(menuAbierto)
		menu.findItem(R.id.action_search).setVisible(false);
	else
		menu.findItem(R.id.action_search).setVisible(true);

	return super.onPrepareOptionsMenu(menu);
}

Con esto ya cumplimos la mayoría de las recomendaciones de la guía de diseño, pero aún nos falta permitir al usuario abrir el menú pulsando sobre el icono de la aplicación de la action bar.

Para ello, al final del método onCreate() habilitaremos la pulsación del icono llamando a los métodos setDisplayHomeAsUpEnabled() y setHomeButtonEnabled(), y añadiremos al evento onOptionsItemSelected() (el encargado de procesar las pulsaciones sobre la action bar, una llamada inicial al método onOptionsItemSelected() del objeto ActionBarDrawerToggle creado anteriormente, de forma que si éste devuelve true (significaría que se ha gestionado una pulsación sobre el icono de la aplicación) salgamos directamente de este método.

public void onCreate(Bundle savedInstanceState) {

	//...

	getSupportActionBar().setDisplayHomeAsUpEnabled(true);
	getSupportActionBar().setHomeButtonEnabled(true);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

	if (mDrawerToggle.onOptionsItemSelected(item)) {
		return true;
	}

	//...
}

Por último, dos indicaciones más incluidas en la documentación oficial de la clase ActionBarDrawerToggle:

  • Implementar el evento onPostCreate() de la actividad, donde llamando al método syncState() del objeto ActionBarDrawerToggle.
  • Implementar el evento onConfigurationChanged() de la actividad, donde llamaremos al método homólogo del objeto ActionBarDrawerToggle.

Veamos cómo quedarían el código de estos dos últimos pasos:

@Override
protected void onPostCreate(Bundle savedInstanceState) {
	super.onPostCreate(savedInstanceState);
	drawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
	super.onConfigurationChanged(newConfig);
	drawerToggle.onConfigurationChanged(newConfig);
}

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 y acciones de la action bar se va actualizando según el estado del menú y la opción seleccionada.

Si por ejemplo lo ejecutamos sobre Android 4 lo veremos como se muestra en las siguientes capturas (sobre Android 2.x se vería de forma casi idéntica). Menú cerrado / Menú abierto:

menu-cerrado   menu-abierto

Espero que estos dos ú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) sin que por ello haya que restringirse a versiones recientes del sistema operativo ni recurrir a librerías externas.

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.