Diseño Android: Menu lateral con Navigation Drawer

logo android

Navigation Drawer es un elemento de interfaz definido por Material Design consistente en el típico menú lateral deslizante, generalmente desde la izquierda. Suele encontrase en la pantalla principal de la app y contar con un botón para desplegarlo aunque también puede ser abierto con un gesto de desplazamiento. Aunque ha sido un tanto denostado en los últimos tiempos y en algunas aplicaciones está siendo reemplazado por la barra Bottom Navigation se sigue utilizando ampliamente.

La siguiente captura muestra el menú actual (diciembre de 2018) de la app de ebay.

Android proporciona una implementación en el módulo design de las librerias de compatibilidad. En este tutorial veremos el uso básico de este componente.

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 9 (Api 28) y es compatible hasta Android 4.4 (Api 19).

En el build.properties del módulo app necesitamos las dependencias de appcompat para la Toolbar y de design para el Navigation Drawer propiamente dicho.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.danielme.android.navigationdrawer"
        minSdkVersion 19
        targetSdkVersion 28
        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.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
}

Cursos aplicaciones móviles

En el diseño de la activity principal, que he llamado HomeActivity, usaremos como layout principal 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"?>
<android.support.v4.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">

        <android.support.v7.widget.Toolbar android:id="@+id/toolbar" style="@style/AppTheme.Toolbar" />

    </LinearLayout>

    <android.support.design.widget.NavigationView android:id="@+id/navigation_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" />

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

En la Activity además de la Toolbar 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.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
    </style>

    <style name="AppTheme.Toolbar">
        <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">4dp</item>
        <item name="android:theme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</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.

  <android.support.design.widget.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. 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ú.

<android.support.design.widget.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));
    FragmentManager fragmentManager = getSupportFragmentManager();
    fragmentManager.beginTransaction().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ú, esto es, el fragment para 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.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

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));
    FragmentManager fragmentManager = getSupportFragmentManager();
    fragmentManager.beginTransaction().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>
   <android.support.design.widget.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. Lo más sencillo será crear un rectángulo de un 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.

     <android.support.design.widget.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

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.