Diseño Android: menu lateral Navigation drawer. Listeners. Navigation Framework.

Última actualización: 10/06/2023
logo android

Navigation Drawer es uno de los componentes visuales definidos por Material Design más importantes. Tal vez su nombre no te diga nada, pero como usuario lo conoces de sobra: el popular menú lateral deslizable desde la izquierda (la derecha en lenguajes RTL) y que suele contar con un botón «hamburguesa» (tres líneas horizontales apiladas) para desplegarlo.

>> Read this post in English here <<

La siguiente captura muestra el menú principal de una de mis apps.

my birds menu drawer

Más pronto que tarde tendrás que incluir este tipo de menú en tus aplicaciones. En este tutorial te explico cómo usar la implementación que Google ofrece con las librerías AndroidX y Material Components. Exploraremos las funcionalidades básicas, así como la integración con Navigation Framework. Todo ello lo haremos a la antigua usanza, esto es, con XML y sin Jetpack Compose.

Nota. Con el paso de los años, el menú lateral pierde terreno frente al menú inferior Bottom Navigation. Google recomienda este último para menús con cinco o menos elementos de navegación. Si quieres añadirlo a tus apps, consulta este artículo:

Contenido

  1. Proyecto con Navigation Drawer
    1. Dependencias
    2. DrawerLayout y NavigationView
    3. Configuración en la Activity
    4. Tema visual
  2. Elementos del menú
  3. Cabecera o header del menú
  4. Implementar las acciones del menú
  5. Integración con AndroidX Navigation framework
  6. Estilos
    1. Colores aplicados
    2. Personalización
  7. Conclusión
  8. Código de ejemplo

Proyecto con Navigation Drawer

Empecemos creando un proyecto de ejemplo con las librerías y herramientas más recientes disponibles en junio de 2023:

  • Android API 33
  • Gradle 8.0 (requiere Java 11)
  • Android Studio Flamingo
  • Material Components 1.9

El lenguaje de programación es Java. No obstante, el código será tan simple que no tendrás problemas en adaptarlo a Kotlin si prefieres este lenguaje.

Advertencia. Quizás encuentres tutoriales y libros que emplean las librerías Support Libraries para construir el menú. Ni caso; es material «prehistórico» en términos tecnológicos. Google abandonó el desarrollo de esas librerías a finales de 2018 y las integró en AndroidX.

Dependencias

Añade la dependencia de Material Components al fichero build.properties:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 33

    defaultConfig {
        applicationId "com.danielme.android.navigationdrawer"
        minSdkVersion 26
        targetSdkVersion 33
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    namespace 'com.danielme.android.navigationdrawer'
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.android.material:material:1.9.0'
}

DrawerLayout y NavigationView

El proyecto de ejemplo tiene una única activity, o actividad, llamada HomeActivity. La raíz de su layout es el widget DrawerLayout. Se trata de un contenedor (ViewGroup) proporcionado por AndroidX que aloja los elementos relacionados con el menú lateral. Pon dentro dos componentes de Material Components:

  • La barra superior de la pantalla, la ActionBar de Android implementada por el componente MaterialToolbar. Está contenida en un LinearLayout en el que más adelante incrustaremos un fragmento (fragment).
  • El componente NavigationView, el menú lateral en sí.

Este es el fichero /res/layout/activity_home.xml con la primera versión del layout (lo retocaremos a lo largo del tutorial):

<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">
 
    <LinearLayout
        android:id="@+id/home_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
 
        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize" />
 
    </LinearLayout>
 
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true" />
 
</androidx.drawerlayout.widget.DrawerLayout>
Configuración en la Activity

En la actividad, además de la Toolbar, debes configurar el DrawerLayout con la clase ActionBarDrawerToggle. Su nombre da una pista de su cometido: integrar el menú del Drawer con la ActionBar. La integración consiste en mostrar el icono hamburguesa en la ActionBar de manera que su pulsación \ toque deslice el menú para hacerlo visible. Si no configuras este comportamiento, el menú solo se abrirá deslizando con el dedo el lado izquierdo de la pantalla hacia la derecha.

El código a escribir, rutinario y reutilizable:

public class HomeActivity extends AppCompatActivity {

  private DrawerLayout drawerLayout;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
 
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(findViewById(R.id.toolbar));
 
    drawerLayout = findViewById(R.id.drawer_layout);
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
           this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawerLayout.addDrawerListener(toggle);
    toggle.syncState();
}

Por lo común, otro comportamiento deseable es el cierre del menú con la pulsación del botón Atrás (Back) de Android.

@Override
 public void onBackPressed() {
   if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
     drawerLayout.closeDrawer(GravityCompat.START);
   } else {
     super.onBackPressed();
   }
 }

Capturas el evento sobrescribiendo el método onBackPressed que hereda la actividad. Su contenido es simple una vez conoces dos métodos de DrawerLayout: el que informa si el menú está abierto (isDrawerOpen) y el que lo cierra (closeDrawer). Cuando el menú esté cerrado, llama a super.onBackPressed() para que la acción Atrás realice el comportamiento predeterminado —navegar un nivel hacia atrás en el historial de pantallas—. Dado que estamos en la pantalla inicial de la aplicación, la pulsación de Atrás con el menú cerrado nos saca de la app.

Tema visual

Ocupémonos de los estilos. En el fichero /res/values/themes.xml he declarado como tema global de la aplicación uno de tipo DayNight de Material Components. Son los que tienen soporte para tema claro y oscuro a la vez. En concreto, he escogido el que no se aplica a la ActionBar del sistema, ya que tenemos nuestra propia Toolbar y le daremos un estilo personalizado (*). Por supuesto, eres libre de elegir el tema que desees.

(*) Explico la configuración en este tutorial.

<?xml version="1.0" encoding="utf-8"?>
 
<resources xmlns:tools="http://schemas.android.com/tools">
 
    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
        <item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.PrimarySurface</item>
        <item name="drawerArrowStyle">@style/AppTheme.DrawerArrowStyle</item>
    </style>
     
    <style name="AppTheme.DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
        <item name="color">@android:color/white</item>
    </style>
 
</resources>

Atención a la propiedad drawerArrowStyle: define el estilo para el icono hamburguesa. Será blanco.

Con los estilos anteriores, el menú parece cubierto por la barra de estado (status bar) de Android.

Si bien el diseño típico muestra el menú debajo de la barra de estado, lo hace aplicando un efecto translúcido. Conseguirlo resulta trivial:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
     <item name="colorPrimary">@color/primary</item>
     <item name="colorPrimaryDark">@color/primaryDark</item>
     <item name="android:statusBarColor">@android:color/transparent</item>
     <item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.PrimarySurface</item>
     <item name="drawerArrowStyle">@style/AppTheme.DrawerArrowStyle</item>
 </style>

¡Enhorabuena! Ya tienes el menú. De momento es un lienzo en blanco al que añadirás los distintos elementos o ítems de navegación.

Elementos del menú

Demos contenido al menú. Lo defines como cualquier menú de Android: en un XML, ubicado en la carpeta /res/menu, con elementos de tipo <item> que se corresponden con cada entrada del menú.

Este es el menú del proyecto de ejemplo (activity_home_navigation_drawer.xml). Define cinco elementos con un texto y un icono:

<?xml version="1.0" encoding="utf-8"?>
 
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">
 
    <group android:checkableBehavior="single">
 
        <item
            android:id="@+id/nav_camera"
            android:icon="@drawable/ic_menu_camera"
            android:title="@string/menu_camera" />
 
        <item
            android:id="@+id/nav_gallery"
            android:icon="@drawable/ic_menu_gallery"
            android:title="@string/menu_gallery" />
 
        <item
            android:id="@+id/nav_tools"
            android:icon="@drawable/ic_menu_manage"
            android:title="@string/menu_tools" />
 
        <item
            android:id="@+id/nav_share"
            android:icon="@drawable/ic_menu_share"
            android:title="@string/menu_share" />
 
        <item
            android:id="@+id/nav_send"
            android:icon="@drawable/ic_menu_send"
            android:title="@string/menu_send" />
 
    </group>
 
</menu>

Escoge títulos descriptivos pero concisos. Ten presente que si un título no cabe en la pantalla, Android lo truncará. Y aunque los iconos son opcionales, facilitan el uso del menú.

Ahora vincula el menú y el componente NavigationView:

<com.google.android.material.navigation.NavigationView
      android:id="@+id/navigation_view"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"
      android:layout_gravity="start"
      android:fitsSystemWindows="true"
      app:menu="@menu/activity_home_navigation_drawer" />

¡Tachán! Aquí está el menú.

Puedes organizar los ítems en secciones, con o sin título, divididas por un pequeño separador. La idea es agrupar los elementos del menú relacionados para que el usuario encuentre rápidamente la opción que necesita, algo recomendable cuando el menú sea extenso.

Cada sección consiste en un bloque <group> con los ítems que abarca. Si la sección precisa de un título, pon su <group> dentro de un bloque <menu> que, a su vez, colocas dentro de un elemento <item> en el que defines el título.

¿Suena complicado? Para que lo veas claro he organizado el menú de ejemplo en tres secciones, la última de ellas con un título:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">
 
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_camera"
            android:icon="@drawable/ic_menu_camera"
            android:title="@string/menu_camera" />
        <item
            android:id="@+id/nav_gallery"
            android:icon="@drawable/ic_menu_gallery"
            android:title="@string/menu_gallery" />
    </group>
 
    <group
        android:id="@+id/group1"
        android:checkableBehavior="single">
        <item
            android:id="@+id/nav_tools"
            android:icon="@drawable/ic_menu_manage"
            android:title="@string/menu_tools" />
    </group>
 
    <item android:title="@string/menu_group">
        <menu>
            <group android:checkableBehavior="single">
                <item
                    android:id="@+id/nav_share"
                    android:icon="@drawable/ic_menu_share"
                    android:title="@string/menu_share" />
 
                <item
                    android:id="@+id/nav_send"
                    android:icon="@drawable/ic_menu_send"
                    android:title="@string/menu_send" />
            </group>
        </menu>
    </item>
 
</menu>

En la siguiente imagen tienes la estructura del menú en el editor visual de Android Studio.

El resultado.

Cabecera o header del menú

En general, los menús laterales disponen de una cabecera (header) que muestra información sobre la aplicación y el usuario. La cabecera es un layout cualquiera, así que contiene lo que quieras. El componente NavigationView la inserta en la parte superior del menú.

A modo de ejemplo, usemos como cabecera el siguiente layout (fichero /res/layout/nav_header.xml). Consiste en un ConstraintLayout que apila dos TextView, uno con el icono y nombre de la aplicación, y otro con un texto de copyright:

<?xml version="1.0" encoding="utf-8"?>
 
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fitsSystemWindows="true"
    android:background="@android:color/holo_blue_dark">
 
    <TextView
        android:id="@+id/header_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/activity_horizontal_margin"
        android:layout_marginTop="@dimen/activity_horizontal_margin"
        android:drawablePadding="12dp"
        android:gravity="center_vertical"
        android:text="@string/app_name"
        android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium.Inverse"
        app:drawableStartCompat="@mipmap/ic_launcher"
        app:layout_constraintTop_toTopOf="parent" />
 
    <TextView
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:text="@string/copyright"
        android:textAppearance="@style/Base.TextAppearance.AppCompat.Small.Inverse"
        app:layout_constraintEnd_toEndOf="@+id/header_title"
        app:layout_constraintStart_toStartOf="@+id/header_title"
        app:layout_constraintTop_toBottomOf="@+id/header_title" />
 
 
</androidx.constraintlayout.widget.ConstraintLayout>

El layout precisa la dependencia de ConstraintLayout:

implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

A continuación, establece la cabecera en la propiedad headerLayout de NavigationMenu:

<com.google.android.material.navigation.NavigationView
        android:id="@+id/navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/activity_home_navigation_drawer" />

Así queda.

En ConstraintLayout activé fitsSystemWindows para que el layout reciba un relleno o padding que lo separe de la barra de estado. De no hacerlo ocurrirá el siguiente efecto indeseable, peor cuanto mayor sea la barra de estado:

android drawer overlap status bar

La cabecera puede ser interactiva y su contenido dinámico, ya que es accesible con el método NavigationView#getHeaderView. Por ejemplo, estas líneas de código muestran un Toast cuando el usuario toque el TextView con el título:

 View header = navigationView.getHeaderView(0);
 header.findViewById(R.id.header_title).setOnClickListener(view -> Toast.makeText(
            HomeActivity.this,
            getString(R.string.title_click),
            Toast.LENGTH_SHORT).show());

Implementar las acciones del menú

El menú es inútil; nada ocurre si pulsas una de sus entradas. Pongamos remedio ipso facto.

Tienes que implementar un oyente (listener) de tipo NavigationView.OnNavigationItemSelectedListene. Su único método recibe el elemento pulsado:

public interface OnNavigationItemSelectedListener {
    
    public boolean onNavigationItemSelected(@NonNull MenuItem item);
  
}

Démosle vida al menú. Mostremos un fragmento (HomeContentFragment), con el nombre del ítem pulsado, añadiéndolo al LinearLayout que contiene la Toolbar. Con otras palabras, nos quedamos en HomeActivity y cambiamos el contenido de la pantalla, así como el título de la Toolbar.

El diseño del fragmento:

<?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:background="@android:color/darker_gray"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text"
        style="@android:style/TextAppearance.Large"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold" />

</LinearLayout>

y su código:

package com.danielme.android.navigationdrawer;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;

public class HomeContentFragment extends Fragment {

  private static final String TEXT_ID = "text_id";

  public static HomeContentFragment newInstance(@StringRes int textId) {
    HomeContentFragment frag = new HomeContentFragment();

    Bundle args = new Bundle();
    args.putInt(TEXT_ID, textId);
    frag.setArguments(args);

    return frag;
  }

  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
  Bundle savedInstanceState) {
    View layout = inflater.inflate(R.layout.home_fragment, container, false);

    if (getArguments() != null) {
      String text = getString(getArguments().getInt(TEXT_ID));
      ((TextView) layout.findViewById(R.id.text)).setText(text);
    } else {
      throw new IllegalArgumentException("Argument " + TEXT_ID + " is mandatory");
    }

    return layout;
  }
}

Los fragmentos deben crearse con su constructor sin argumentos. HomeContentFragment sigue la técnica recomendada del constructor estático (*) (método newInstance) para encapsular la creación de sus objetos y la posterior transferencia de los argumentos de entrada.

(*) Técnica también útil fuera del ámbito de los fragmentos. Se explica en el primer capítulo del fantástico libro Effective Java 3rd edition.

El método onCreateView recupera el texto a mostrar y lo pone en el TextView.

Veamos lo más interesante: implementar OnNavigationItemSelectedListener. Por comodidad, he creado el oyente dentro de HomeActivity:

public class HomeActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {

 @Override
  public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
    int titleId = getTitle(menuItem);
    showFragment(titleId);
    drawerLayout.closeDrawer(GravityCompat.START);
    return true;
  }

  private int getTitle(@NonNull MenuItem menuItem) {
    switch (menuItem.getItemId()) {
      case R.id.nav_camera:
        return R.string.menu_camera;
      case R.id.nav_gallery:
        return R.string.menu_gallery;
      case R.id.nav_tools:
        return R.string.menu_tools;
      case R.id.nav_share:
        return R.string.menu_share;
      case R.id.nav_send:
        return R.string.menu_send;
      default:
        throw new IllegalArgumentException("menu option not implemented!!");
    }

  private void showFragment(@StringRes int titleId) {
    Fragment fragment = HomeContentFragment.newInstance(titleId);
    getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.anim.nav_enter, R.anim.nav_exit)
            .replace(R.id.home_content, fragment)
            .commit();

    setTitle(getString(titleId));
  }

}

Aunque aparece bastante código, no tiene nada de especial. getTitle contiene el típico switch que evalúa las opciones de un menú. En el ejemplo decide el texto que mostrará HomeContentFragment. Usando ese texto, el método showFragment construye ese fragmento, lo incrusta en home_content y cambia el título de la Toolbar de la actividad.

También debes asociar el oyente anterior al NavigationView en el onCreate:

NavigationView navigationView = findViewById(R.id.navigation_view);
navigationView.setNavigationItemSelectedListener(this);

Falta un último detalle: la selección de la pantalla inicial. Al arrancarse, HomeActivity debe mostrar el primer elemento del menú (o el que tu quieras), y que en el ejemplo es el fragmento para la opción cámara. Consigues este comportamiento simulando la pulsación del elemento deseado con este código sito en el método onCreate:

MenuItem menuItem = navigationView.getMenu().getItem(0);
onNavigationItemSelected(menuItem);
menuItem.setChecked(true);

Ahora sí: un auténtico menú de navegación 😍

Además de la selección de un elemento, puedes recibir otros cuatro eventos asociados al menú implementando un oyente de tipo DrawerLayout.DrawerListener:

public class HomeActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener,
        DrawerLayout.DrawerListener {

    @Override
    public void onDrawerSlide(@NonNull View view, float v) {
      //cambio en la posición del drawer
    }
 
   @Override
   public void onDrawerOpened(@NonNull View view) {
      //el drawer se ha abierto completamente
     Toast.makeText(this, getString(R.string.navigation_drawer_open),
            Toast.LENGTH_SHORT).show();
   }
 
    @Override
    public void onDrawerClosed(@NonNull View view) {
     //el drawer se ha cerrado completamente
    }
 
    @Override
    public void onDrawerStateChanged(int i) {
       //cambio de estado, puede ser STATE_IDLE, STATE_DRAGGING or STATE_SETTLING
    } 

De nuevo, debes vincular el oyente al componente que genera los eventos. En esta ocasión se trata del DrawerLayout:

drawerLayout.addDrawerListener(this);

Así quedan los métodos más relevantes de HomeActivity tras los últimos cambios:

public class HomeActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener,
        DrawerLayout.DrawerListener {

  private DrawerLayout drawerLayout;
  private NavigationView navigationView;
  private Toolbar toolbar;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
    setupToolbar();
    setupDrawer();
  }

  private void setupToolbar() {
    toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(findViewById(R.id.toolbar));
  }

  private void setupDrawer() {
    drawerLayout = findViewById(R.id.drawer_layout);
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
            this, drawerLayout, toolbar, R.string.navigation_drawer_open,
            R.string.navigation_drawer_close);
    drawerLayout.addDrawerListener(toggle);
    toggle.syncState();

    drawerLayout.addDrawerListener(this);

    setupNavigationView();
  }

  private void setupNavigationView() {
    navigationView = findViewById(R.id.navigation_view);
    navigationView.setNavigationItemSelectedListener(this);
    setDefaultMenuItem();
    setupHeader();
  }

  private void setDefaultMenuItem() {
    MenuItem menuItem = navigationView.getMenu().getItem(0);
    onNavigationItemSelected(menuItem);
    menuItem.setChecked(true);
  }

Integración con AndroidX Navigation framework

El framework Navigation de AndroidX (a partir de ahora lo llamaré Navigation) te ofrece una forma sofisticada y elegante de configurar la navegación desde el menú lateral —y, en general, en toda la aplicación—. Su propósito es simplificar y estandarizar la gestión de la navegación por las pantallas. Defines esa navegación en un fichero XML editable con una herramienta visual, y con una API integras la navegación con los componentes gráficos implicados.

No profundizaré en Navigation; su estudio queda fuera del alcance de este modesto tutorial. Me centraré en cómo integrarlo con NavigationDrawer, de tal modo que se encarge de la navegación que acabamos de implementar en un NavigationView.OnNavigationItemSelectedListener.

Antes de nada, agrega estas dependencias. Contienen las librerias de Navigation para fragmentos:

  • Java:
implementation 'androidx.navigation:navigation-fragment:2.5.3'
implementation 'androidx.navigation:navigation-ui:2.5.3'
  • Kotlin:
implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
implementation "androidx.navigation:navigation-ui-ktx:2.5.3"

Este el grafo de navegación, declarado en el fichero /res/navigation/nav.xml.

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav"
    app:startDestination="@id/nav_camera">

    <fragment
        android:id="@+id/nav_camera"
        android:name="com.danielme.android.navigationdrawer.HomeContentFragment"
        android:label="@string/menu_camera"
        tools:layout="@layout/home_fragment">
        <argument
            android:name="text_id"
            android:defaultValue="@string/menu_camera"
            app:argType="reference" />
    </fragment>

    <fragment
        android:id="@+id/nav_gallery"
        android:name="com.danielme.android.navigationdrawer.HomeContentFragment"
        android:label="@string/menu_gallery"
        tools:layout="@layout/home_fragment">
        <argument
            android:name="text_id"
            android:defaultValue="@string/menu_gallery"
            app:argType="reference" />
    </fragment>

    <fragment
        android:id="@+id/nav_tools"
        android:name="com.danielme.android.navigationdrawer.HomeContentFragment"
        android:label="@string/menu_tools"
        tools:layout="@layout/home_fragment">
        <argument
            android:name="text_id"
            android:defaultValue="@string/menu_tools"
            app:argType="reference" />
    </fragment>

    <fragment
        android:id="@+id/nav_share"
        android:name="com.danielme.android.navigationdrawer.HomeContentFragment"
        android:label="@string/menu_share"
        tools:layout="@layout/home_fragment">
        <argument
            android:name="text_id"
            android:defaultValue="@string/menu_share"
            app:argType="reference" />
    </fragment>

    <fragment
        android:id="@+id/nav_send"
        android:name="com.danielme.android.navigationdrawer.HomeContentFragment"
        android:label="@string/menu_send"
        tools:layout="@layout/home_fragment">
        <argument
            android:name="text_id"
            android:defaultValue="@string/menu_send"
            app:argType="reference" />
    </fragment>

</navigation>

Como puedes ver, en el fichero se indican las cinco pantallas correspondientes a las cinco entradas del menú. Aunque el ejemplo se basa en fragmentos, también podemos usar actividades. Cada pantalla es un «destino» con estos valores:

  • android:id. El identificador del destino de la navegación.
  • android:name. El nombre completo de la clase que gestiona el destino.
  • android:label. El nombre del destino. Lo mostraremos en la ActionBar.
  • tools:layout. El layout del fragmento, dato informativo para Android Studio.

Recuerda que todas las pantallas son el mismo fragmento; solo cambia el texto que muestran en su interior. Dado que el fragmento recibe el texto como un argumento, usé bloques <argument> para declarar el nombre de ese argumento y su valor.

En la línea seis hay un detalle relevante: startDestination establece la pantalla inicial de la navegación, esto es, la que quieres mostrar cuando el usuario abra tu aplicación.

El fichero de navegación no sirve de nada, a menos que lo integres con la interfaz gráfica, el menú y la actividad. En el primer caso, debes añadir a activity_home.xml el componente NavHostFragment. Permite a Navigation incrustar los fragmentos:

<LinearLayout
    android:id="@+id/home_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav" />

</LinearLayout>

Declaras NavHostFragment en un FragmentContainerView. En este último debes activar defaultNavHost para que FragmentContainerView gestione la pulsación del botón Atrás o back del sistema. También debes indicar en la propiedad navGraph el fichero que define la navegación.

En lo que atañe al menú, debes asociar cada una de sus entradas con su pantalla de destino. Tan sencillo como hacer que el identificador de la entrada (el item) coincida con el identificador en el fichero nav.xml que tenga la pantalla (el fragment) que deba mostrar.

Las dificultades surgen al integrar Navigation con el código de la actividad. Esta es la nueva versión del método setupDrawer:

  private void setupDrawer() {
    drawerLayout = findViewById(R.id.drawer_layout);

    NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    navigationView = findViewById(R.id.navigation_view);
    NavigationUI.setupWithNavController(navigationView, navController);

    appBarConfiguration =
            new AppBarConfiguration.Builder(R.id.nav_camera, R.id.nav_gallery, R.id.nav_tools, R.id.nav_send, R.id.nav_share)
                    .setOpenableLayout(drawerLayout)
                    .build();

    NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

    drawerLayout.addDrawerListener(this);
  }

El código anterior lo he escrito tomando como referencia el mostrado por documentación oficial. La clave reside en las líneas resaltadas. La línea siete asocia NavigationView con Navigation empleando una de las sobrecargas del método estático setupWithNavController de NavigationUI. Las líneas previas obtienen los argumentos de entrada de ese método.

Lo anterior no basta porque Navigation debe tomar el control de la Toolbar para establecer el título y mostrar el icono hamburguesa. Esto lo consigues llamando a una sobrecarga del setupActionBarWithNavController, otro método estático de NavigationUI (línea 14). El objeto AppBarConfiguration (línea 9) que recibe los construyes con AppBarConfiguration.Builder indicando la lista de pantallas (línea 10) consideradas de primer nivel, en nuestro caso todas. Sin esa lista, la Toolbar mostrará el icono Atrás en lugar del icono hamburguesa.

Con respecto a lo último, debes sobrescribir el método onSupportNavigateUp de la actividad para que la pulsación del icono hamburguesa abra el menú. Sin pudor alguno, copio el código mostrado en la documentación:

@Override
public boolean onSupportNavigateUp() {
  NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
  return NavigationUI.navigateUp(navController, appBarConfiguration)
          || super.onSupportNavigateUp();
}

Tras los cambios anteriores, el funcionamiento de la aplicación de ejemplo no varía, pero ahora centralizas y configuras la navegación en el fichero nav.xml. Además de este beneficio, el nuevo código escrito en la actividad compensa el que te ahorras: la implementación del oyente, la configuración de ActionBarDrawerToggle y el método setupNavigationView. Incluso en el caso específico de nuestro ejemplo puedes prescindir del método HomeContentFragment#newInstance, pues la creación del fragmento ya no te concierne.

En conclusión, es una buena idea dejar que Navigation haga el trabajo sucio.

Nota. En el proyecto alojado en GitHub el código relativo a Navigation está comentado para que la navegación sea realizada por OnNavigationItemSelectedListener.

Estilos

Como todos los componentes gráficos proporcionados por Google, el menú lateral se integra a las mil maravillas con los estilos y temas de Material Design \ Material Components. El objetivo es que sin apenas configuración nuestras aplicaciones tengan un aspecto armonioso y coherente.

Colores aplicados

El elemento seleccionado aparece resaltado con un fondo de color grisáceo. El título y el icono aparecen tintados con el color principal (colorPrimary) de la aplicación definido en el fichero themes.xml. El fondo del menú toma el color de colorSurface, y el icono y texto de los elementos no seleccionados son del color colorOnSurface.

En este enlace encontrarás los ejemplos oficiales de temas y estilos para Navigation drawer.

Personalización

Gracias a ciertas propiedades de NavigationView algunos rasgos visuales del menú se personalizan con suma facilidad. Por ejemplo, en itemIconTint e itemTextColor puedes establecer unos drawable con los colores que tendrán el icono y el texto en función del estado de la entrada del menú.

El siguiente selector (/res/drawable/drawer_selector.xml) define un color diferente para los dos estados básicos (seleccionado y no seleccionado):

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@android:color/holo_orange_light" android:state_checked="true" />
    <item android:color="@android:color/holo_orange_dark" />
</selector>

Asocia el selector al NavigationView de modo que se aplique tanto al texto como al icono. Así, tendrán siempre el mismo color:

<com.google.android.material.navigation.NavigationView
     android:id="@+id/navigation_view"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:layout_gravity="start"
     android:fitsSystemWindows="true"
     app:headerLayout="@layout/nav_header"
     app:itemIconTint="@drawable/drawer_selector"
     app:itemTextColor="@drawable/drawer_selector"
     app:menu="@menu/activity_home_navigation_drawer" />

Admito que la elección de colores resulta estrambótica y sin embargo es una decisión consciente —tengo mal gusto, pero no tanto—. Quiero que sea llamativa para que veas cómo se aplican los colores.

Como cabría esperar, el fondo de las entradas del menú también es configurable. Eso sí, exige algo más de trabajo porque lo normal es que quieras definirlo como una forma en un drawable. Para ello, sigue estos tres pasos:

  1. Diseña la forma. En el ejemplo, un rectángulo de color azulado (fichero /res/drawable/drawer_bakground_highlight.xml):
<?xml version="1.0" encoding="utf-8"?>
 
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <solid android:color="@android:color/holo_blue_bright" />
</shape>
  1. Crea un selector que muestre el drawable anterior cuando el elemento al que se aplique el selector esté seleccionado:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/drawer_bakground_highlight" android:state_checked="true" />
</selector>
  1. Por último, establece el selector en la propiedad itemBackground de NavigationView:
<com.google.android.material.navigation.NavigationView
   android:id="@+id/navigation_view"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:layout_gravity="start"
   android:fitsSystemWindows="true"
   app:headerLayout="@layout/nav_header"
   app:itemBackground="@drawable/drawer_background_selector"
   app:itemIconTint="@drawable/drawer_selector"
   app:itemTextColor="@drawable/drawer_selector"
   app:menu="@menu/activity_home_navigation_drawer" />

Ya lo tienes.

Conclusión

Eso fue todo. Crear un menú lateral de tipo Navigation Drawer con las características básicas que se esperan de él es relativamente sencillo. Y aunque necesitas escribir algo de código, será más o menos el mismo en todas las aplicaciones. Asimismo, has visto cómo beneficiarte de Navigation.

Te dejo un video con la aplicación de ejemplo que hemos construído.

Código de ejemplo

El proyecto de ejemplo se encuentra en GitHub. Para más información sobre cómo utilizar GitHub, consulta este artículo.

Otros tutoriales Android

Toolbar, pestañas y ViewPager2 con AndroidX y Material Components

Menú inferior emergente con Bottom Sheet y Material Design

EditText con TextInputLayout y Material Components

Barra de menú inferior BottomNavigationView. Listener, badges y Navigation UI.

6 comentarios sobre “Diseño Android: menu lateral Navigation drawer. Listeners. Navigation Framework.

  1. Muchas gracias por el demo, soy educador, y deseo crear una app para visualizar mi sitio web, no sé nada de programación de android studio ¿sabes como incluirle una webview? he visto que que la programación cambia de acuerdo al programador.

  2. Hola, muchas gracias por el ejemplo, soy estudiante y acabo de terminar un módulo de Android. He implementado todo el código poro no consigo que que se ejecuten las acciones desde las opciones de este menú. Lo he repasado de arriba a abajo durante horas y nada.

    1. Acabo de ejecutar el proyecto tal cual está en GitHub en el emulador para Android Api 30 con un Android Studio que acabo de instalar en Windows 10 y funciona correctamente. Definitivamente hay algún detalle que se te escapa…

  3. Hola, sabes como abrir Facebook, Twitter, YouTube, etc. desde un icono del navigationdrawer? Me seria de mucha ayuda, gracias

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.