Interfaz de Usuario en Android:
Navigation Drawer (NavigationView)
En este nuevo tema vamos a tratar otro de los componentes relacionados con
la interfaz 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 tema 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).
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
simplemente 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):
• El layout real de la actividad, que en casos como este suele pasar a
llamarse ‘content.layout’.
• 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 nuestro caso de ejemplo quedaría como sigue
(/res/layout/activity_main.xml):
1 <android.support.v4.widget.DrawerLayout
2 xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/drawer_layout"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:fitsSystemWindows="true"
9 tools:context=".MainActivity">
10
11 <!-- Layout real de la actividad -->
12 <include layout="@layout/content_layout" />
13
14 <!-- Layout del menú lateral (Navigation View) -->
15 <android.support.design.widget.NavigationView
16 android:id="@+id/navview"
17 android:layout_width="wrap_content"
18 android:layout_height="match_parent"
19 android:fitsSystemWindows="true"
20 android:layout_gravity="start"
21 app:headerLayout="@layout/header_navview"
22 app:menu="@menu/menu_navview" />
23
24 </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 tema 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 este caso, para no complicar el ejemplo, vamos 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.
1
2
3
4
5
6
7 <FrameLayout
8 xmlns:android="http://schemas.android.com/apk/res/android"
9 android:layout_width="match_parent"
android:layout_height="match_parent">
1
0
<ImageView
1
android:layout_width="match_parent"
1
android:layout_height="200dp"
1
android:src="@drawable/navheader"
2
android:scaleType="centerCrop" />
1
3
<TextView
1
android:layout_width="wrap_content"
4
android:layout_height="wrap_content"
1 android:text="@string/usuario"
5 android:textAppearance="@style/TextAppearance.AppCompat.Large.Invers
1 e"
6 android:textStyle="bold"
1 android:layout_gravity="bottom"
7 android:layout_marginBottom="10dp"
1 android:layout_marginLeft="10dp" />
8 </FrameLayout>
1
9
2
0
2
1
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:
1 <menu xmlns:android="http://schemas.android.com/apk/res/android">
2 <group android:checkableBehavior="single">
3 <item
4 android:id="@+id/menu_seccion_1"
5 android:icon="@drawable/ic_menu"
6 android:title="@string/seccion_1"/>
7 <item
8 android:id="@+id/menu_seccion_2"
9 android:icon="@drawable/ic_menu"
10 android:title="@string/seccion_2"/>
11 <item
12 android:id="@+id/menu_seccion_3"
13 android:icon="@drawable/ic_menu"
14 android:title="@string/seccion_3"/>
15 </group>
16
17 <item
18 android:id="@+id/navigation_subheader"
19 android:title="@string/otras_opciones">
20 <menu>
21 <item
22 android:id="@+id/menu_opcion_1"
23 android:icon="@drawable/ic_menu"
24 android:title="@string/opcion_1"/>
25 <item
26 android:id="@+id/menu_opcion_2"
27 android:icon="@drawable/ic_menu"
28 android:title="@string/opcion_2"/>
29 </menu>
30 </item>
31 </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 línea
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 nuestro caso se ha
utilizado por simplicidad el mismo icono en todas las opciones, pero por
supuesto pueden ser distintos.
En una imagen veremos mejor el resultado:
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 mostrados en los temas 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):
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:tools="http://schemas.android.com/tools"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:orientation="vertical"
6 android:clickable="true"
7 tools:context=".MainActivity"
8 tools:showIn="@layout/activity_main">
9
10 <!-- Toolbar -->
11 <android.support.v7.widget.Toolbar
12 xmlns:app="http://schemas.android.com/apk/res-auto"
13 android:id="@+id/appbar"
14 android:layout_height="?attr/actionBarSize"
15 android:layout_width="match_parent"
16 android:minHeight="?attr/actionBarSize"
17 android:background="?attr/colorPrimary"
18 android:elevation="4dp"
19 android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
20 app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
21
22 <!-- Resto de la interfaz de usuario -->
23 <FrameLayout
24 android:id="@+id/content_frame"
25 android:layout_width="match_parent"
26 android:layout_height="match_parent" />
27
28 </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
tema 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>
1
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
2
<item name="colorPrimary">@color/colorPrimary</item>
3
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
4
<item name="colorAccent">@color/colorAccent</item>
5
6
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
7
<item
8 name="android:statusBarColor">@android:color/transparent</item>
9 </style>
10 </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
1
xmlns:android="http://schemas.android.com/apk/res/android"
2
xmlns:tools="http://schemas.android.com/tools"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent"
5
tools:context="dam.miguel.android.a23navigationdrawer.Fragment1">
6
7
<TextView
8
android:layout_width="match_parent"
9 android:layout_height="match_parent"
10 android:text="@string/fragment1" />
11
12 </FrameLayout>
Y su clase java asociada (Fragment1.java), que se limitará a inflar el
layout anterior:
1 import android.os.Bundle;
2 import android.support.v4.app.Fragment;
3 import android.view.LayoutInflater;
4 import android.view.View;
5 import android.view.ViewGroup;
6
7 public class Fragment1 extends Fragment {
8
9 public Fragment1() {
10 // Required empty public constructor
11 }
12
13 @Override
14 public View onCreateView(LayoutInflater inflater, ViewGroup container,
15 Bundle savedInstanceState) {
16 // Inflate the layout for this fragment
17 return inflater.inflate(R.layout.fragment_fragment1, container,
18 false);
19 }
}
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.
1 drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
2 navView = (NavigationView)findViewById(R.id.navview);
3
4 navView.setNavigationItemSelectedListener(
5 new NavigationView.OnNavigationItemSelectedListener() {
6 @Override
7 public boolean onNavigationItemSelected(MenuItem menuItem) {
8
9 boolean fragmentTransaction = false;
10 Fragment fragment = null;
11
12 switch (menuItem.getItemId()) {
13 case R.id.menu_seccion_1:
14 fragment = new Fragment1();
15 fragmentTransaction = true;
16 break;
17 case R.id.menu_seccion_2:
18 fragment = new Fragment2();
19 fragmentTransaction = true;
20 break;
21 case R.id.menu_seccion_3:
22 fragment = new Fragment3();
23 fragmentTransaction = true;
24 break;
25 case R.id.menu_opcion_1:
26 Log.i("NavigationView", "Pulsada opción 1");
27 break;
28 case R.id.menu_opcion_2:
29 Log.i("NavigationView", "Pulsada opción 2");
30 break;
31 }
32
33 if(fragmentTransaction) {
34 getSupportFragmentManager().beginTransaction()
35 .replace(R.id.content_frame, fragment)
36 .commit();
37
38 menuItem.setChecked(true);
39 getSupportActionBar().setTitle(menuItem.getTitle());
40 }
41
42 drawerLayout.closeDrawers();
43
44 return true;
45 }
46 });
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
onNavigationItemSelected. En 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 este caso de ejemplo nos limitamos 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 el que se ha
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().
1 @Override
2 protected void onCreate(Bundle savedInstanceState) {
3
4 //...
5
6 appbar = (Toolbar)findViewById(R.id.appbar);
7 setSupportActionBar(appbar);
8
9 getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_nav_menu);
10 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
11
12 //...
13 }
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:
1 @Override
2 public boolean onOptionsItemSelected(MenuItem item) {
3
4 switch(item.getItemId()) {
5 case android.R.id.home:
6 drawerLayout.openDrawer(GravityCompat.START);
7 return true;
8 //...
9 }
10
11 return super.onOptionsItemSelected(item);
12 }
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:
Por último del todo. El DrawerLayout dispone de una serie de eventos que
podemos cazar y actuar en consecuencia si así lo necesitáramos:
//Eventos del Drawer Layout
drawerLayout.setDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
Cuando deslizamos con el dedo
}
@Override
public void onDrawerOpened(View drawerView) {
Cuando se abre el drawer
}
@Override
public void onDrawerClosed(View drawerView) {
Cuando se cierra el drawer
}
@Override
public void onDrawerStateChanged(int newState) {
Con cualquier cambio de estado
}});