Diseño Android: Menu lateral con Navigation Drawer y AndroidX

Última actualización: 18/11/2020

logo android

Navigation Drawer es un elemento de interfaz definido por Material Design consistente en el típico menú lateral deslizante desde la izquierda (salvo en lenguajes RTL) y utilizado ampliamente como el principal elemento de navegación de las aplicaciones móviles. Suele encontrase en la pantalla principal de la app y contar con un botón para desplegarlo aunque también puede ser abierto deslizando el contenido de la pantalla desde el extremo izquierdo. A veces se suele complementar con el menú inferior Bottom Navigation.

La siguiente captura muestra el menú actual de una de mis apps

my birds menu drawer

Google proporcionó una implementación en el módulo design de las Librería de compatibilidad. Pero estas librerias están obsoletas y en su lugar usaremos las versiones equivalentes de esos mismos widgets incluidas AndroidX para crear un menú de navegación.

Proyecto con Navigation Drawer

Empecemos el tutorial creando un proyecto de ejemplo con una única Activity, y su correspondiente Toolbar, que mostrará un menú de tipo Navigation Drawer. El proyecto tendrá como target Android 11 (Api 30) y es compatible hasta Android 4.4 (Api 19).

En el build.properties del módulo app sólo necesitamos la dependencia de Material Components.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

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

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

Cursos aplicaciones móviles

En el diseño de la activity principal, que he llamado HomeActivity, usaremos como layout DrawerLayout el cual contendrá tanto el interfaz principal de la pantalla, consistente en un layout (home_content) con la Toolbar dentro del cual mostrará un fragment correspondiente a la opción seleccionada en el menú, y el componente NavigationView que es el menú propiamente dicho.

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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/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" style="@style/AppTheme.Toolbar" />

    </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>

En la Activity, además de la Toolbar que hace de ActionBar de la app, configuramos el DrawerLayout utilizando la clase ActionBarDrawerToggle para integrar el menú con la Toolbar gracias al icono popularmente conocido como hamburguer cuya pulsación muestra el menú. Si no implementamos este comportamiento, el menú sólo se mostrará deslizando la pantalla hacia la derecha.

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

    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar((Toolbar) 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();
  }

Al pulsarse el botón back si el menú está desplegado debería ocultarse. Este comportamiento es necesario implementarlo en la Activity.

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

Por último, echemos un vistazo a los estilos. Aquí definimos un tema global de la app que no utiliza la ActionBar del sistema ya que estamos definiendo nuestra propia Toolbar, así como los estilos propios de la Toolbar. Toda esta configuración se describe en el tutorial correspondiente.

<?xml version="1.0" encoding="utf-8"?>

<resources xmlns:tools="http://schemas.android.com/tools">

    <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
        <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
    </style>

    <style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.Primary">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">?attr/actionBarSize</item>
        <item name="android:background">@color/primary</item>
        <item name="android:elevation" tools:targetApi="lollipop">@dimen/toolbar_elevation</item>
    </style>

</resources>

Con estos estilos el menú da la sensación de aparecer debajo de la status bar siendo ocultado por la misma.

El efecto habitual consiste en mostrar el menú debajo de la status bar con un efecto translúcido y podemos conseguirlo haciendo la status bar transparente.

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
        <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
    </style>

Ya tenemos completado el ejemplo base sobre el que iremos desarrollando el menú.

Elementos del menú

En esta sección del tutorial vamos a dar contenido al menú. Este contenido se define de forma similar a cualquier menú Android en un xml ubicado en la carpeta /res/menu con elementos de tipo item que se corresponden con cada entrada del menú.

<?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_manage" 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>

Este menú se enlaza con el Navigation Drawer del siguiente modo.

  <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" />

Los items pueden ser agrupados fácilmente en secciones con título o sin título y que se dividen automáticamente mediante un pequeño separador. En el primer caso hay que crear un nuevo menú dentro de un elemento item, en el segundo caso basta con definir un nuevo grupo con su identificador correspondiente.

<?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_manage"
            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>

Cabecera del menú

Habitualmente los menús laterales disponen de una cabecera o header que muestra información sobre la app y el usuario. Este header no es más que un layout que el componente NavigationView inserta en la parte superior del menú. A modo de ejemplo usemos el siguiente header consistente en un LinearLayout que apila un TextView con el icono y nombre de la app y otro TextView con un texto de copyright.

<?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="@dimen/nav_header_height" android:background="@android:color/holo_blue_dark" android:gravity="bottom" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin">

    <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:drawableStart="@mipmap/ic_launcher" android:drawablePadding="12dp" android:gravity="center_vertical" android:text="@string/app_name" android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium.Inverse" />

    <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/activity_vertical_margin" android:text="@string/copyright" android:textAppearance="@style/Base.TextAppearance.AppCompat.Small.Inverse" />

</LinearLayout>

Se añade al menú.

<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" />

Se puede acceder directamente desde código al layout del header para gestionarlo programáticamente si fuera necesario. Por ejemplo, mostremos un Toast cuando el usuario pulse el TextView con el título.

  View header = navigationView.getHeaderView(0);
    header.findViewById(R.id.header_title).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Toast.makeText(HomeActivity.this, getString(R.string.title_click),
                Toast.LENGTH_SHORT).show();
      }
    });

Eventos

La captura y tratamiento de la selección de un elemento del menú se realiza en una implementación del contrato o interfaz NavigationView.OnNavigationItemSelectedListener. En el ejemplo voy a mostrar un fragment dentro de HomeActivity que muestre un texto con el nombre de la opción seleccionada, dicho nombre también será utilizado como título en la Toolbar. El listener es implementado por la propia Activity.

  @Override
  public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
    int title;
    switch (menuItem.getItemId()) {
      case R.id.nav_camera:
        title = R.string.menu_camera;
        break;
      case R.id.nav_gallery:
        title = R.string.menu_gallery;
        break;
      case R.id.nav_manage:
        title = R.string.menu_tools;
        break;
      case R.id.nav_share:
        title = R.string.menu_share;
        break;
      case R.id.nav_send:
        title = R.string.menu_send;
        break;
      default:
        throw new IllegalArgumentException("menu option not implemented!!");
    }

    Fragment fragment = HomeContentFragment.newInstance(getString(title));
    getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.anim.bottom_nav_enter, R.anim.bottom_nav_exit)
            .replace(R.id.home_content, fragment)
            .commit();


    setTitle(getString(title));

    drawerLayout.closeDrawer(GravityCompat.START);

    return true;
  }

Este listener debe ser asociado al NavigationView.

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

Al iniciarse la Activity debe mostrarse por defecto el primer elemento del menú que en nuestro ejemplo es el fragment correspondiente a la opción Cámara. Vamos a simular programáticamente la pulsación de dicho elemento con el siguiente código dentro del método onCreate de la Activity.

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

Además de la selección de un elemento, implementando un DrawerLayout.DrawerListener podemos recibir otros eventos asociados al Navigation Drawer.

  @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
  }

Este listener debe ser asociado a DrawerLayout.

drawerLayout.addDrawerListener(this);

El código completo de HomeActivity queda así.

package com.danielme.android.navigationdrawer;

import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;

import com.google.android.material.navigation.NavigationView;

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

  private DrawerLayout drawerLayout;

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

    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar((Toolbar) 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();

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

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

    drawerLayout.addDrawerListener(this);

    View header = navigationView.getHeaderView(0);
    header.findViewById(R.id.header_title).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Toast.makeText(HomeActivity.this, getString(R.string.title_click),
                Toast.LENGTH_SHORT).show();
      }
    });
  }

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

  @Override
  public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
    int title;
    switch (menuItem.getItemId()) {
      case R.id.nav_camera:
        title = R.string.menu_camera;
        break;
      case R.id.nav_gallery:
        title = R.string.menu_gallery;
        break;
      case R.id.nav_manage:
        title = R.string.menu_tools;
        break;
      case R.id.nav_share:
        title = R.string.menu_share;
        break;
      case R.id.nav_send:
        title = R.string.menu_send;
        break;
      default:
        throw new IllegalArgumentException("menu option not implemented!!");
    }

    Fragment fragment = HomeContentFragment.newInstance(getString(title));
    getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.anim.bottom_nav_enter, R.anim.bottom_nav_exit)
            .replace(R.id.home_content, fragment)
            .commit();

    setTitle(getString(title));

    drawerLayout.closeDrawer(GravityCompat.START);

    return true;
  }

  @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
  }

}

Estilos

El elemento seleccionado del menú se muestra destacado con un background de color grisáceo mientras que el título y el icono quedan tintados con el color principal (colorPrimary) de la app definido en el styles.xml.

Estos estilos pueden ser cambiados fácilmente gracias a los atributos itemIconTint y itemTextColor del widget NavigationView. En ambos casos podemos definir un drawable con los colores para los estados seleccionado y normal. Por ejemplo

<?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>
   <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" />

El background del item se puede modificar de forma similar pero requiere algo más de trabajo ya que en lugar de un color el background hay que modelarlo como una forma en un drawable. Vamos a utilizar un rectángulo de color sólido.

<?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>

A continuación creamos el selector y sólo definimos el estado seleccionado con el drawable anterior.

<?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>

Por último aplicamos el selector al NavigationView utilizando la propiedad itemBackground.

     <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" />

Ejemplo final

Código de ejemplo

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

Master Pyhton, Java, Scala or Ruby

Un comentario sobre “Diseño Android: Menu lateral con Navigation Drawer y AndroidX

  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.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

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