Diseño Android: Menu inferior Bottom Navigation con Material Components

logo android

La especificación de Material Design incluye un tipo de menú de navegación que en los últimos tiempos está adquiriendo gran popularidad y que encontramos en apps tan conocidas como Spotify o Instagram, además de las aplicaciones propias de Google tales como YouTube o Google Play. Se trata de una barra de navegación situada en la parte inferior de la pantalla que muestra con iconos y, opcionalmente, textos, entre tres y cinco elementos de navegación. Asimismo, en muchas apps esté menú se complementa como un menú lateral.

bottomnavigationview examples

Google proporcionó una implementación de este componente con el widget BottomNavigationView en el módulo design de la ya obsoleta librería de compatibilidad. En este tutorial vamos a utilizar la última versión de BottomNavigationView que es la disponible en Material Components For Android.

Proyecto de ejemplo

El proyecto de ejemplo, sencillo pero realista, consiste en una única Activity con una Toolbar y tendrá como target Android 11 (Api 30) siendo compatible hasta Android 4.4 (Api 19). En el build.properties del módulo app necesitamos la dependencia de Material Components.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.danielme.android.bottomnavigation"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
}

Cursos aplicaciones móviles

Los elementos que aparecerán en la barra de navegación se configuran como cualquier menú estándar. Vamos a utilizar los siguientes cuatro.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/page_home"
        android:icon="@drawable/baseline_home_black_24"
        android:title="@string/bottom_nav_home" />
    <item
        android:id="@+id/page_fav"
        android:icon="@drawable/baseline_favorite_black_24"
        android:title="@string/bottom_nav_fav" />
    <item
        android:id="@+id/page_search"
        android:icon="@drawable/baseline_search_black_24"
        android:title="@string/bottom_nav_search" />
    <item
        android:id="@+id/page_settings"
        android:icon="@drawable/baseline_app_settings_alt_black_24"
        android:title="@string/bottom_nav_settings" />
</menu>

Los iconos los he descargado de la página oficial de Material Design.

Ahora vamos con la pantalla. La primera versión simplemente mostrará una Toolbar en la parte superior y el menú anterior en la parte inferior gracias al widget BottomNavigationView.

<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        style="@style/AppTheme.ToolbarMain"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

La barra de navegación tiene la configuración mínima necesaria y simplemente se ha indicado el menú. El resultado es el siguiente.

android bottomnavigation default

Podemos comprobar que por defecto las entradas del menú sólo muestran el icono y el título queda reservado para el elemento seleccionado. Al seleccionarse un elemento también se aplica el efecto correspondiente. Este comportamiento puede modificarse con la propiedad labelVisibilityMode

  • app:labelVisibilityMode=”unlabeled” nunca se mostrará el texto del icono.

    android bottomnavigation unlabeled

  • app:labelVisibilityMode=”labeled” se muestra siempre el texto. Es el diseño más habitual.
    android bottomnavigation labeled

En lo que respecta a los colores, la barra se muestra con el color definido por la variable ?attr/colorSurface y el elemento seleccionado se resalta utilizando el primary color de la app. Los estilos son muy configurables y las propiedades que podemos personalizar están bien descritas al final de la documentación oficial.

Implementando los eventos

Ahora vamos a proceder a implementar en la Activity que gestiona la pantalla las pulsaciones en las entradas del menú. Cada una mostrará un fragment con una imagen correspondiente al elemento pulsado. Siempre usaremos el mismo fragment y le pasaremos como un parámetro el identificador del icono (drawable a mostrar).

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

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        tools:srcCompat="@drawable/baseline_app_settings_alt_black_48" />

</FrameLayout>

La creación la haremos con el típico “constructor estático” ya que un fragment debe crearse siempre utilizando su constructor vacío, a menos que trabajemos con FragmentFactory. El fragment va a “inflar” el layout y establecer el icono en el ImageView.

package com.danielme.android.bottomnavigation;

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

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

public class PageFragment extends Fragment {

  private static final String ARG_ICON = "ARG_ICON";

  static PageFragment newInstance(@DrawableRes int iconId) {
    PageFragment frg = new PageFragment();

    Bundle args = new Bundle();
    args.putInt(ARG_ICON, iconId);
    frg.setArguments(args);

    return frg;
  }

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

    layout.findViewById(R.id.imageView).setBackgroundResource(getArguments().getInt(ARG_ICON));

    return layout;
  }

}

Y ahora añadimos un “contenedor” en la activity para insertar el fragment.

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        style="@style/AppTheme.ToolbarMain"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
        app:layout_constraintTop_toBottomOf="@id/toolbar" />


    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"

Tenemos que asociar un listener de tipo OnNavigationItemSelectedListener a nuestro BottomMenu. La implementación es muy sencilla pues simplemente mostraremos en pantalla el fragment con el icono deseado haciendo la transición con una animación, pero hay “truco”: al cargarse la pantalla hay que mostrar el fragment correspondiente al elemento del menú seleccionado por defecto.

  private void setupBottomMenu() {
    bottomNavigationView = findViewById(R.id.bottom_navigation);
    bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
      switch (item.getItemId()) {
        case R.id.page_home:
          showFragment(PageFragment.newInstance(R.drawable.baseline_home_black_48));
          break;
        case R.id.page_fav:
          showFragment(PageFragment.newInstance(R.drawable.baseline_favorite_black_48));
          break;  
        case R.id.page_search:
          showFragment(PageFragment.newInstance(R.drawable.baseline_search_black_48));
          break;
        case R.id.page_settings:
          showFragment(PageFragment.newInstance(R.drawable.baseline_app_settings_alt_black_48));
          break;
        default:
          throw new IllegalArgumentException("item not implemented : " + item.getItemId());
      }
     return true;
    });
    //setear aquí para que el listener muestre el fragment inicial al cargarse la pantalla
    bottomNavigationView.setSelectedItemId(R.id.page_home);
  }

  private void showFragment(Fragment frg) {
     getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.anim.bottom_nav_enter, R.anim.bottom_nav_exit)
            .replace(R.id.container, frg)
            .commit();

  }

Otro detalle a tener en cuenta es que cuando rotamos la pantalla deberíamos conservar la selección para evitar que se muestre en pantalla el fragment inicial pero aparezca seleccionado el que había anteriormente.

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(SELECTION, bottomNavigationView.getSelectedItemId());
  }

  private void setupBottomMenu(Bundle savedInstanceState) {
    bottomNavigationView = findViewById(R.id.bottom_navigation);
    bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
     ...
      }
    });
     //setear aquí para que el listener muestre el fragment inicial al cargarse la pantalla
    if (savedInstanceState == null) {
      bottomNavigationView.setSelectedItemId(R.id.page_home);
    } else {
      bottomNavigationView.setSelectedItemId(savedInstanceState.getInt(SELECTION));
    }
  }

Badges

Las entradas de menú pueden mostrar en su parte superior derecha un pequeño indicador para llamar la atención del usuario con cierta información. Por ejemplo, informar que en la sección de notificaciones tiene mensajes pendientes de leer.

bottomnavigationview badge

Con el widget BadgeDrawable de las librerías de Material Componentes podemos aplicar fácilmente esta funcionalidad añadiendo a BottomNavigationView los BadgeDrawable que deseemos, asociándolos a la entrada de menú correspondiente.

 bottomNavigationView.getOrCreateBadge(R.id.page_fav).setNumber(1000);
 bottomNavigationView.getOrCreateBadge(R.id.page_settings).setVisible(true);

android bottomnavigation badget

Ocultar\mostrar con scroll

Aunque no lo he visto en muchas aplicaciones, Material Design recoge la posibilidad de ocultar\mostrar automáticamente la barra de navegación al realizarse scroll en el contenido principal de la pantalla. Este comportamiento lo proporciona la integración de BottomNavigationView con CoordinatorLayout y se configura de forma análoga a otros componentes gráficos como los botones FAB o las estructuras basadas en AppbarLayout (toolbar con pestañas, imágenes de fondo, etc) que también proporcionan de serie integración directa con CoordinatorLayout.

Vamos a ampliar la app de ejemplo con una nueva pantalla que muestre un listado con un RecyclerView. Puesto que tengo publicado un par de tutoriales sobre RecyclerView y su utilización no forma parte de los objetivos didácticos del presente tutorial, no voy a detenerme en los detalles de su implementación.

  1. Añadir la dependencia
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.google.android.material:material:1.2.1'
        implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
        implementation "androidx.recyclerview:recyclerview:1.1.0"
        coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
    }
    
  2. Creamos el layout para el Fragment que contendrá el RecyclerView
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
    </FrameLayout>
    
  3. Cada fila del listado consiste en un TextView
  4. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/selectableItemBackground"
        android:gravity="center_vertical"
        android:minHeight="@dimen/list_height">
    
        <TextView
            android:id="@+id/textView"
            style="@android:style/TextAppearance.Medium"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:paddingStart="@dimen/standard_margin"
            android:paddingEnd="@dimen/standard_margin"
            tools:text="item" />
    </FrameLayout>
    
  5. En el Adapter recibimos una lista de cadenas que mapeamos con el ViewHolder correspondiente al layout anterior
    package com.danielme.android.bottomnavigation;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import androidx.recyclerview.widget.RecyclerView;
    
    import java.util.List;
    
    public class SimpleTextRecyclerViewAdapter extends RecyclerView.Adapter<SimpleTextRecyclerViewAdapter.ViewHolder> {
    
      private final List<String> items;
      private final LayoutInflater inflater;
    
      SimpleTextRecyclerViewAdapter(Context context, List<String> items) {
        this.inflater = LayoutInflater.from(context);
        this.items = items;
      }
    
      @Override
      public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.row_recycler_view, parent, false);
        return new ViewHolder(view);
      }
    
      @Override
      public int getItemCount() {
        return items.size();
      }
    
      @Override
      public void onBindViewHolder(ViewHolder holder, int position) {
        holder.getTextView().setText(items.get(position));
      }
    
      public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    
        private final TextView textView;
    
        ViewHolder(View itemView) {
          super(itemView);
          textView = itemView.findViewById(R.id.textView);
          itemView.setOnClickListener(this);
        }
    
        public TextView getTextView() {
          return textView;
        }
    
        @Override
        public void onClick(View view) {
          Toast.makeText(view.getContext(), items.get(getAdapterPosition()), Toast.LENGTH_SHORT).show();
        }
      }
    
    }
    
  6. Y la implementación del fragment propiamente dicho
  7. package com.danielme.android.bottomnavigation;
    
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import androidx.annotation.Nullable;
    import androidx.fragment.app.Fragment;
    import androidx.recyclerview.widget.DividerItemDecoration;
    import androidx.recyclerview.widget.LinearLayoutManager;
    import androidx.recyclerview.widget.RecyclerView;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class RecyclerViewFragment extends Fragment {
    
      @Override
      public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
              Bundle savedInstanceState) {
        View layout = inflater.inflate(R.layout.fragment_recycler_view, container, false);
    
        RecyclerView recyclerView = layout.findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
    
        List<String> items = new ArrayList<>();
        for (int i = 0; i < 25; i++) {
          items.add("item " + i);
        }
    
        recyclerView.setAdapter(new SimpleTextRecyclerViewAdapter(getContext(), items));
    
        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
                DividerItemDecoration.VERTICAL);
        recyclerView.addItemDecoration(dividerItemDecoration);
    
        return layout;
      }
    
    }
    
  8. Para mostrarlo volvemos a lo visto al inicio del tutorial y añadimos un botón a la barra de navegación.
    <item
            android:id="@+id/page_list"
            android:icon="@drawable/baseline_list_black_24"
            android:title="@string/bottom_nav_list" />
    
  9. Y finalizamos el desarrollo lanzando el fragment dentro del listener del NavigationBottomView.
     bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
          switch (item.getItemId()) {
            case R.id.page_home:
              showFragment(PageFragment.newInstance(R.drawable.baseline_home_black_48));
              break;
            case R.id.page_list:
              showFragment(new RecyclerViewFragment());
              break;
            ...
    

Ya podemos ver el listado pero la barra de navegación siempre está visible. Para ocultarla automáticamente, recurrimos como dije anteriormente a la “magia” de CoordinatorLayout y tenemos que refactorizar el layout de la Activity de tal modo que nuestro BottomNavigationView sea hijo directo del mismo. La ocultación se aplicará a todos los fragments cuyo contenido sea scrollable (recyclerview, scrollview).

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinatort"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        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.ToolbarMain" />

        <FrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </LinearLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:labelVisibilityMode="labeled"
        app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"
        app:menu="@menu/bottom_nav_menu" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

También se puede ocultar del mismo modo la Toolbar al hacer scroll utilizando un AppBarLayout como “padre” de la Toolbar.

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinatort"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            style="@style/AppTheme.ToolbarMain"
            app:layout_scrollFlags="scroll|enterAlways" />

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:labelVisibilityMode="labeled"
        app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"
        app:menu="@menu/bottom_nav_menu" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

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.

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 .