En este artículo aprenderás a crear una aplicación Android que contenga Navigation Drawer y Tabs de forma conjunta. La idea es crear secciones con diferentes fragmentos y ver como introducir una gran variedad de layouts.
El ejemplo que verás se trata de una aplicación que ofrece a los usuarios diferentes platos de comida pertenecientes a un restaurante hipotético llamado «Rico Pa’ Rico».
Crearemos cuatro secciones del navigation drawer para comprobar cómo se sortean situaciones similares a estas:
- Un navigation drawer con fragmentos diferentes.
- Un navigation drawer con tabs.
- Listas y grid views en tabs.
- Tabs con fragmentos diferentes.
- Iniciar una actividad de configuración desde un navigation drawer.
Si sigues leyendo podrás obtener un resultado similar a este:
Para ver el link de descarga del proyecto completo en Android Studio, sigue estas instrucciones:
[sociallocker id=»7121″]
#1. Planeación De Pantallas Y Sus Relaciones
Antes de comenzar con el desarrollo, conozcamos un poco más de la app.
Con la aplicación móvil Rico Pa’ Rico se pretende mostrar:
- Un inicio con los productos más populares entre los clientes del restaurante.
- Los datos de la cuenta del usuario como su perfil, las direcciones que tiene asociadas para la entrega de productos a domicilio y la información de las tarjetas que ha usado para pagos.
- También se necesita mostrar el menú de comidas divididos en tres categorías: Platillos, Bebidas y Postres. Sería ideal mostrar el precio y el nombre más una pequeña imagen de la comida.
- Posibilitar la configuración del envío de correos informativos a la cuenta del usuario. Las frecuencias de envío son: diario, semanal, mensual o sin envío.
Básicamente esos son los requerimientos de la app. Como ves, hacen falta varias funcionalidades para tener un servicio a domicilio completo, sin embargo, estas características permitirán entender a la perfección el uso de un navigation drawer y tabs.
A partir de los requerimientos se deduce que se necesita la siguiente lista de pantallas:
- Menú principal para las secciones disponibles
- Inicio con una lista de los elementos populares
- Sección para los datos de la cuenta
- Sección para las categorías
- Pantalla de configuración de notificaciones
- Detalle del perfil del usuario
- Lista de direcciones
- Lista de tarjetas o pantalla con aviso en blanco
- Grilla de platillos
- Grilla para bebidas
- Grilla para postres
Ahora representemos las relaciones entre pantallas a través de un mapa relacional. La idea es mostrar los posibles accesos desde una pantalla hacia otra.
El siguiente paso es decidir los patrones convenientes para representar la estructura del mapa anterior. Es aquí donde encaja el patrón del Navigation Drawer.
¿Pero por qué usar un navigation drawer en esta aplicación?
Porque existen algunas secciones que tienen dos niveles de navegación o incluso la forma en que estaría organizado el modelo de datos de esta aplicación muestra subdivisión en los datos.
En ese caso la documentación de Material Design sugiere añadir este elemento como primer nivel y dejar en segundo nivel las subcagtegorías a través de tabs.
En primer nivel estarían cuatro categorías principales asociadas al funcionamiento de la aplicación como lo son Inicio, Mi Cuenta, Categorías (o Menú) y Configuración.
Dentro de la cuenta podemos crear varias subdivisiones entre el perfil, las direcciones y las tarjetas. Este sería un segundo nivel.
Al igual que en categorías, donde encontramos valga la redundancia, tres subcategorías para segmentar.
Teniendo esto claro, dentro del mapa de pantallas podemos reemplazar los elementos Menú Principal, Mi Cuenta y Categorías por los patrones necesarios.
#2. Wireframe Aplicación Android
El segundo paso es bocetar las ideas generales de funcionalidad que tenemos. Para ello usaré una aplicación online gratuita para crear wireframes de apps móviles llamada Ninja Mock.
Su funcionamiento es sencillo. Simplemente te registras y luego abres tu escritorio de apps. Una vez allí presionas New Project para crear un nuevo proyecto.
En seguida seleccionas la categoría ANDROID.
Y con ello se abrirá tu espacio de trabajo para que realices los diseños necesarios. Tendrás a disposición gran variedad de elementos visuales en el panel izquierdo, así como la posibilidad de editar sus propiedades en el panel derecho.
Sin embargo este apartado no es un tutorial sobre ninja mock. Espero esta introducción te facilite probar sus funcionalidades.
Nota: Si quieres aprender más sobre wireframing y diseño de apps móviles, entonces te recomiendo tomar el curso Curso de diseño de aplicaciones para iOs y Android.
Basado en el mapa de pantallas con patrones definidos de la anterior sección, el boceto de nuestra app quedaría de la siguiente forma:
Ahora lo que sigue es crear cada uno de los componentes gráficos necesarios para generar la aplicación.
#3. Navigation Drawer Con Diferentes Fragmentos
Cada una de las pantallas de nuestra aplicación será contenida en una serie de fragmentos relacionados dentro del navigation drawer.
Solo tendremos dos actividades para manejar la estructura completa. Una actividad principal y la de ajustes o configuración.
Comencemos…
1. Abre Android Studio y crea un nuevo proyecto llamado «Restaurante Rico Pa Rico». Añade por defecto una actividad en blanco con el nombre ActividadPrincipal.java
y confirma.
2. Prepara las características mínimas del proyecto para su funcionamiento. Define la paleta de colores para material design dentro de tu archivo colors.xml de la siguiente forma:
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="window_background">#ffffff</color> <color name="primaryDarkColor">#323232</color> <color name="primaryColor">#484848</color> <color name="accentColor">#F87652</color> <color name="background_footer_item_grid_categorias">#90000000</color> <color name="color_light">#ffffff</color> <color name="background_header_seccion_inicio">#4B4B4B</color> </resources>
No te preocupes por los demás valores, ya que los usaremos más adelante.
Ahora añade los siguientes strings para la interfaz.
strings.xml
<resources> <string name="app_name">"Restaurante Rico Pa Rico "</string> <string name="action_settings">Configuración</string> <string name="action_search">Buscar</string> <string name="action_carrito">Añadir al carrito</string> <string name="item_inicio">Inicio</string> <string name="item_cuenta">Mi Cuenta</string> <string name="item_categorias">Categorías</string> <string name="item_configuracion">Configuración</string> <string name="titulo_comidas_populares">Lo más pedido</string> <string name="titulo_tab_platillos">PLATILLOS</string> <string name="titulo_tab_bebidas">BEBIDAS</string> <string name="titulo_tab_postres">POSTRES</string> <string name="titulo_tab_perfil">PERFIL</string> <string name="titulo_tab_direcciones">DIRECCIONES</string> <string name="titulo_tab_tarjetas">TARJETAS</string> <string name="texto_no_tarjetas">No tienes ninguna tarjeta asociada</string> <string name="etiqueta_carrito">Carrito</string> <string name="texto_cambiar_contrasena">Cambia tu contraseña</string> <string name="etiqueta_contrasena">Contraseña</string> <string name="etiqueta_info_usuario">Información del usuario</string> </resources>
Importante declarar los estilos que usaremos. En nuestro caso tendremos dos estilos. Uno para la actividad principal, la cual usará el navigation drawer y requiere transparencia. Y otro para la actividad configuración, la cual usa el estilo por defecto.
styles.xml
<resources> <style name="Theme.RicoPaRico" parent="Base.AppTheme" /> <style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="colorPrimary">@color/primaryColor</item> <item name="colorPrimaryDark">@color/primaryDarkColor</item> <item name="colorAccent">@color/accentColor</item> <item name="android:windowBackground">@color/window_background</item> </style> <style name="Theme.ConNavigationDrawer" parent="Base.AppTheme"/> </resources>
Para versiones menores a 21, los atributos de transparencia no son soportados, por lo que dejamos el tema Theme.ConNavigationDrawer
vacío. Sin embargo, en versiones mayores o iguales a 21 si requeriremos completar este estilo.
values-21/styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="Theme.RicoPaRico" parent="Base.AppTheme"> </style> <style name="Theme.ConNavigationDrawer" parent="Base.AppTheme"> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:statusBarColor">@android:color/transparent</item> </style> </resources>
Por último añade las dependencias de las librerías que usaremos en el proyecto. Debido al tipo de estilo visual que usaremos, requeriremos la nueva librería de diseño, el uso de glide para carga de imágenes, la extensión de recycler view y cardview.
build.gradle
dependencies { ... compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.android.support:design:23.0.1' compile 'com.android.support:recyclerview-v7:23.0.1' compile 'com.github.bumptech.glide:glide:3.6.1' compile 'com.android.support:support-v4:23.0.1' compile 'com.android.support:cardview-v7:23.0.1' }
¡Ah! y muy importante, descarga los drawables del proyecto para completar la interfaz. A continuación el link de descarga.
3. Abre el layout de la actividad principal (en mi caso se llama actividad_principal.xml) y elimina su diseño por defecto para comenzar a construir uno basado en un navigation drawer.
Para tener una guía observaremos el diseño del wireframe bocetado anteriormente:
En este caso, es un navigation drawer con cabecera. Solo tiene 4 ítems con las secciones estudiadas.
La cabecera contiene el logo de la empresa en la parte superior alineada a la izquierda y por debajo una doble línea que representa la cantidad existente dentro del carrito de compras (incluso tiene un icono en la parte izquierda para aclarar la idea).
La creación es sencilla como vimos en el artículo NavigationView: Navigation Drawer Con Material Design, donde el nodo principal es un elemento DrawerLayout
. Cuyo contenido han de ser dos elementos.
El contenido principal, representado por cualquier elemento visual y un componente NavigationView
(librería de soporte de diseño) para la parte deslizante con el menú.
actividad_principal.xml
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <!-- Contenido Principal --> <include layout="@layout/contenido_principal" /> <!-- Menú Deslizante --> <android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/cabecera_drawer" app:menu="@menu/menu_drawer" /> </android.support.v4.widget.DrawerLayout>
4. El header contenido principal lo declararemos en un layout externo llamado contenido_principal.xml. Dentro de él pondremos una estructura simple que contenga la App Bar con una Toolbar implementada y como contenedor principal irá un RelativeLayout
, el cual será reemplazado por cada fragmento de las secciones.
contenido_principal.xml
<LinearLayout 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="match_parent" android:orientation="vertical"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.AppBarLayout> <RelativeLayout android:id="@+id/contenedor_principal" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
5. El header del navigation drawer lo crearemos en otro layout independiente llamado cabecera_drawer.xml. Básicamente necesitamos un linear layout que contenga una fila para el logo (ImageView
) y otra para un TableLayout de 2×2 para ubicar el icono del carrito más la etiqueta y texto asociadas a las compras.
cabecera_drawer.xml
<?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="wrap_content" android:background="@color/primaryColor" android:gravity="bottom" android:orientation="vertical" android:padding="@dimen/padding_izquierdo_cabecera" android:theme="@style/ThemeOverlay.AppCompat.Dark"> <!-- Logo Rico Pa' Rico --> <ImageView android:id="@+id/imageView3" android:layout_width="@dimen/ancho_logo" android:layout_height="wrap_content" android:src="@drawable/logo" /> <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:columnCount="2" android:orientation="vertical" android:rowCount="2"> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/icono_carrito" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="8dp" android:src="@drawable/carrito_compras" /> <TextView android:id="@+id/etiqueta_carrito" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="@string/etiqueta_carrito" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textStyle="bold" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/texto_total_carrito" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="1" android:layout_gravity="center_vertical" android:text="$ 0" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textStyle="bold" /> </TableRow> </TableLayout> </LinearLayout>
6. Lo siguiente es construir un recurso del tipo menu para implementar las secciones del navigation drawer. Para ello crea un nuevo archivo llamado menu_drawer.xml.
Recuerda que tendremos las siguientes secciones o ítems: Inicio, Mi Cuenta, Categorías y Configuración.
menu_drawer.xml
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/item_inicio" android:checked="true" android:icon="@drawable/inicio" android:title="@string/item_inicio" /> <item android:id="@+id/item_cuenta" android:icon="@drawable/cuenta" android:title="@string/item_cuenta" /> <item android:id="@+id/item_categorias" android:icon="@drawable/categorias" android:title="@string/item_categorias" /> <item android:id="@+id/item_configuracion" android:icon="@drawable/configuracion" android:title="@string/item_configuracion" /> </group> </menu>
7. Por último modifica la lógica de la actividad principal.
Cambia el home button por el icono del navigation drawer y luego controla el evento de apertura dentro de onOptionsItemSelected()
. Recuerda que esto lo hacemos el método DrawerLayout.openDrawer()
.
ActividadPrincipal.java
package com.herprogramacion.restaurantericoparico.ui; import android.os.Bundle; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import com.herprogramacion.restaurantericoparico.R; public class ActividadPrincipal extends AppCompatActivity { private DrawerLayout drawerLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.actividad_principal); agregarToolbar(); drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); } private void agregarToolbar() { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); final ActionBar ab = getSupportActionBar(); if (ab != null) { // Poner ícono del drawer toggle ab.setHomeAsUpIndicator(R.drawable.drawer_toggle); ab.setDisplayHomeAsUpEnabled(true); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_actividad_principal, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: drawerLayout.openDrawer(GravityCompat.START); return true; } return super.onOptionsItemSelected(item); } }
Al ejecutar la aplicación tendrás algo como esto:
#4. Sección De Navigation Drawer Con Fragmento De Lista
Siguiendo el orden, ahora es turno de crear la sección Inicio, al cual contiene una lista de las comidas más populares que tiene el restaurante. El boceto se ve así:
Abordar este diseño es fácil para nosotros, ya que sabemos muy bien cómo usar el RecyclerView para crear listas pobladas por adaptadores.
Básicamente necesitamos:
- La clase Fragment para el contenido.
- Un layout para el fragmento.
- Un modelo de datos.
- Un layout para los ítems.
- Un adaptador para el recycler view.
Así que manos a la obra.
1. Añade un nuevo fragmento en blanco al proyecto llamado FragmentoInicio.java. Recuerda que puedes hacerlo a través del asistente File > New > Fragment > Fragment (Blank).
2. Abre el layout asignado el fragmento. En mi caso se llama fragmento_inicio.xml. La idea es añadir un encabezado inicial con el texto "Lo más pedido"
y por debajo un recycler view que muestre la lista de elementos.
fragmento_inicio.xml
<?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:orientation="vertical"> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:background="@color/background_header_seccion_inicio" android:gravity="center" android:padding="8dp" android:text="@string/titulo_comidas_populares" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textColor="@color/color_light" /> <android.support.v7.widget.RecyclerView android:id="@+id/reciclador" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" /> </LinearLayout>
3. Crea una nueva clase Java llamada Comida
. Su objetivo es representar cada comida que mostraremos a través de la aplicación, es decir, nuestra fuente de datos.
Según las especificaciones del boceto, simplemente necesitamos información sobre el precio, el nombre y el recurso de la imagen que representará su exquisitez.
Comida.java
package com.herprogramacion.restaurantericoparico.modelo; import com.herprogramacion.restaurantericoparico.R; import java.util.ArrayList; import java.util.List; /** * Modelo de datos estático para alimentar la aplicación */ public class Comida { private float precio; private String nombre; private int idDrawable; public Comida(float precio, String nombre, int idDrawable) { this.precio = precio; this.nombre = nombre; this.idDrawable = idDrawable; } public static final List<Comida> COMIDAS_POPULARES = new ArrayList<Comida>(); public static final List<Comida> BEBIDAS = new ArrayList<>(); public static final List<Comida> POSTRES = new ArrayList<>(); public static final List<Comida> PLATILLOS = new ArrayList<>(); static { COMIDAS_POPULARES.add(new Comida(5, "Camarones Tismados", R.drawable.camarones)); COMIDAS_POPULARES.add(new Comida(3.2f, "Rosca Herbárea", R.drawable.rosca)); COMIDAS_POPULARES.add(new Comida(12f, "Sushi Extremo", R.drawable.sushi)); COMIDAS_POPULARES.add(new Comida(9, "Sandwich Deli", R.drawable.sandwich)); COMIDAS_POPULARES.add(new Comida(34f, "Lomo De Cerdo Austral", R.drawable.lomo_cerdo)); } public float getPrecio() { return precio; } public String getNombre() { return nombre; } public int getIdDrawable() { return idDrawable; } }
El miembro COMIDAS_POPULARES
será el flujo de datos que tendremos para poblar el adaptador.
4. Con los datos ya preparados ahora podemos crear el layout de los ítems, así que añade un nuevo archivo llamado item_lista_inicio.xml.
Usaremos un FrameLayout
para sobreponer el precio y el nombre sobre la imagen de cada elemento.
item_lista_inicio.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/miniatura_comida" android:layout_width="match_parent" android:layout_height="@dimen/altura_miniatura_comida" android:scaleType="centerCrop" /> <TextView android:id="@+id/precio_comida" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left|bottom" android:layout_marginBottom="32dp" android:paddingLeft="@dimen/espacio_norma_1" android:text="Large Text" android:textColor="@android:color/white" android:textSize="48sp" android:textStyle="italic" /> <TextView android:id="@+id/nombre_comida" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left|bottom" android:paddingBottom="16dp" android:paddingLeft="16dp" android:text="Medium Text" android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textColor="@android:color/white" /> </FrameLayout>
5. Crea una nueva clase llamada AdaptadorInicio.java para implementar el adaptador del recycler view para el inicio. Esta parte es muy fácil, solo no olvides alimentarlo con la lista COMIDAS_POPULARES
.
AdaptadorInicio.Java
package com.herprogramacion.restaurantericoparico.ui; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.herprogramacion.restaurantericoparico.R; import com.herprogramacion.restaurantericoparico.modelo.Comida; /** * Adaptador para mostrar las comidas más pedidas en la sección "Inicio" */ public class AdaptadorInicio extends RecyclerView.Adapter<AdaptadorInicio.ViewHolder> { public static class ViewHolder extends RecyclerView.ViewHolder { // Campos respectivos de un item public TextView nombre; public TextView precio; public ImageView imagen; public ViewHolder(View v) { super(v); nombre = (TextView) v.findViewById(R.id.nombre_comida); precio = (TextView) v.findViewById(R.id.precio_comida); imagen = (ImageView) v.findViewById(R.id.miniatura_comida); } } public AdaptadorInicio() { } @Override public int getItemCount() { return Comida.COMIDAS_POPULARES.size(); } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.item_lista_inicio, viewGroup, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { Comida item = Comida.COMIDAS_POPULARES.get(i); Glide.with(viewHolder.itemView.getContext()) .load(item.getIdDrawable()) .centerCrop() .into(viewHolder.imagen); viewHolder.nombre.setText(item.getNombre()); viewHolder.precio.setText("$" + item.getPrecio()); } }
6. Con todos los componentes creados ya es posible poblar el recycler view desde el fragmento.
FragmentoInicio.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; /** * Fragmento para la sección de "Inicio" */ public class FragmentoInicio extends Fragment { private RecyclerView reciclador; private LinearLayoutManager layoutManager; private AdaptadorInicio adaptador; public FragmentoInicio() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragmento_inicio, container, false); reciclador = (RecyclerView) view.findViewById(R.id.reciclador); layoutManager = new LinearLayoutManager(getActivity()); reciclador.setLayoutManager(layoutManager); adaptador = new AdaptadorInicio(); reciclador.setAdapter(adaptador); return view; } }
7. Finalmente prepararemos el navigation drawer para seleccionar elementos y responder a los eventos creando el nuevo fragmento de inicio.
En primera instancia obtén la instancia del NavigationView en el método onCreate() de la actividad principal.
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
Ahora comprueba en ese mismo lugar, si este está vacio. Si no es así, entonces prepara el navigation view y luego selecciona su ítem por defecto.
if (navigationView != null) { prepararDrawer(navigationView); // Seleccionar item por defecto seleccionarItem(navigationView.getMenu().getItem(0)); }
Para mí preparar el drawer significa setear la escucha de selección y configurar todos los atributos antes de iniciar con la selección. Para ello utilizo el método prepararDrawer().
private void prepararDrawer(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { menuItem.setChecked(true); seleccionarItem(menuItem); drawerLayout.closeDrawers(); return true; } }); }
Recuerda onNavigationItemSelected
nos provee un controlador para la selección. Dentro de este recibimos un objeto MenuItem
para extraer toda la información del item seleccionado. Con setChecked()
sombreamos el elemento seleccionado si usas true como parámetro.
La selección implica crear el contenido necesario de la sección, por lo que tenemos el método seleccionarItem()
para ello.
Y como acción lógica una vez seleccionado cierra el drawer con closeDrawers()
para seguir una secuencia limpia.
El método seleccionarItem() tiene un switch para determinar qué tipo de fragmento se creará una vez comparado el valor. Algo como esto:
private void seleccionarItem(MenuItem itemDrawer) { Fragment fragmentoGenerico = null; FragmentManager fragmentManager = getSupportFragmentManager(); switch (itemDrawer.getItemId()) { case R.id.item_inicio: fragmentoGenerico = new FragmentoInicio(); break; case R.id.item_cuenta: // Fragmento para la sección Cuenta break; case R.id.item_categorias: // Fragmento para la sección Categorías break; case R.id.item_configuracion: // Iniciar actividad de configuración break; } if (fragmentoGenerico != null) { fragmentManager .beginTransaction() .replace(R.id.contenedor_principal, fragmentoGenerico) .commit(); } // Setear título actual setTitle(itemDrawer.getTitle()); }
Todo se define con el identificador del ítem que se obtiene a través de getItemId()
. Este podemos compararlo con las constantes que tenemos en el archivo R.java.
Al final se reemplaza el contenido del RelativeLayout
identificado por R.id.contenedor_principal
que tenemos en actividad_principal.xml y luego se cambia el título de la actividad con el método getTitle()
.
Si corres el proyecto deberías ver la siguiente imagen:
#5. Tabs Con Diferentes Fragmentos
Dentro de la sección Mi Cuenta
es necesario usar tabs con fragmentos que tienen diferentes diseños. Por ejemplo, el diseño del perfil implementa dos cards para mostrar los datos de la cuenta.
Sin embargo en las direcciones y tarjetas veremos distintas interfaces.
¿Cómo implementar este diseño en un TabLayout
sabiendo que tiene distintos fragmentos?
De cierta manera, este asunto no tiene por qué afectar nuestro desarrollo, ya que el adaptador de fragmentos que usa a la hora de poblar un ViewPager
puede recibir una lista de elementos genéricos de la clase Fragment
. Lo que quiere decir que su método de inserción aceptará cualquier objeto que herede de esta clase.
El problema de noción está a la hora de incluir un ViewPager
en el layout del fragmento. ¿Crees que es posible?
¡Claro que si!
En la documentación de Android se menciona que desde Android 4.2 es posible usar fragmentos anidados en tus aplicaciones, es decir, fragmentos dentro de otros fragmentos.
Por esta razón podemos incrustar el patrón Swipe Views dentro del layout de un fragmento con un soporte desde versiones 1.6.
Así que la idea de incorporar pestañas basadas en un fragmento es viable. En el caso de la sección mi cuenta necesitaremos:
- Un Fragmento para las páginas de las pestañas y su layout correspondiente
- Un fragmento para la sección PERFIL
- Un fragmento para la sección DIRECCIONES
- Un fragmento para la sección TARJETAS
También es posible crear un navigation drawer con diferentes actividades por sección para mantener el control de los layouts. Sin embargo ese será tema de un artículo futuro.
Veamos…
1. Añade un nuevo fragmento al proyecto y nómbralo FragmentoCuenta.java.
2. Abre su archivo de diseño y borrar el contenido por defecto. La idea es hacer de este fragmento el contenedor de las páginas que se coordinarán con las pestañas de la sección Mi Cuenta.
Como vimos en el artículo TabLayout: ¿Cómo Añadir Pestañas En Android?, si deseamos tener un efecto deslizante entre páginas de contenido debes añadir un ViewPager
que ocupe el espacio total del fragmento.
fragmento_paginado.xml
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" />
3. El enfoque que estamos usando de solamente fragmentos dificulta insertar pestañas de forma descriptiva a través del layout del fragmento actual, ya que la App Bar se encuentra en el layout de la actividad principal. El actual fragmento lo que hace es reemplazar un contenedor de segundo nivel.
Así que para añadir las pestañas debemos hacer uso del diseño programático. Esto quiere decir que crearemos una nueva instancia de un objeto TabLayout
y luego lo incrustaremos en la App Bar que está un nivel más arriba de la jerarquía de views.
La ventaja es que el método onCreateView() del fragmento trae en sus parámetros el contenedor padre de la jerarquía de views. Con este podemos buscar la instancia fácilmente y luego añadir las pestañas a través del método addView().
import android.graphics.Color; import android.os.Bundle; import android.support.design.widget.AppBarLayout; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; import java.util.ArrayList; import java.util.List; /** * Fragmento de la sección "Mi Cuenta" */ public class FragmentoCuenta extends Fragment { private AppBarLayout appBar; private TabLayout pestanas; public FragmentoCuenta() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragmento_paginado, container, false); if (savedInstanceState == null) { insertarTabs(container); } return view; } private void insertarTabs(ViewGroup container) { View padre = (View) container.getParent(); appBar = (AppBarLayout) padre.findViewById(R.id.appbar); pestanas = new TabLayout(getActivity()); pestanas.setTabTextColors(Color.parseColor("#FFFFFF"), Color.parseColor("#FFFFFF")); appBar.addView(pestanas); } @Override public void onDestroyView() { super.onDestroyView(); appBar.removeView(pestanas); } }
Como ves, el método insertarTabs()
es el encargado de añadir un nuevo TabLayout
en la App Bar. Este recibe como parámetro el contenedor del fragmento que en este caso es el layout de la actividad principal.
Luego se obtiene una instancia de la app bar, consiguiendo el padre del contenedor a través de getParent()
(en contenido_principal.xml sería el elemento identificado con R.id.contenedor_principal
) que sería un LinearLayout
.
Esto permitirá añadir el view sin ningún problema. Obviamente al añadirlo, también hay que quitarlo al momento en que el fragmento se destruya con removeView()
.
Al ejecutar tendrás algo como:
4. A continuación crearemos el fragmento para la página PERFIL. Simplemente añade una nueva clase llamada FragmentoPerfil
.
FragmentoPerfil.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; /** * Fragmento para la pestaña "PERFIL" De la sección "Mi Cuenta" */ public class FragmentoPerfil extends Fragment { public FragmentoPerfil() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragmento_perfil, container, false); } }
Y luego diseña las tarjetas para los datos del usuario y la contraseña como se vió en el boceto al inicio.
fragmento_perfil.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#dee4ee" android:orientation="vertical" android:padding="@dimen/espacio_norma_1"> <TextView android:id="@+id/titulo_informacion_usuario" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="8dp" android:text="@string/etiqueta_info_usuario" android:textAppearance="?android:attr/textAppearanceSmall" /> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" card_view:cardCornerRadius="2dp" card_view:cardUseCompatPadding="true"> <GridLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:columnCount="2" android:padding="@dimen/espacio_norma_1" android:rowCount="2"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginBottom="32dp" android:layout_marginRight="@dimen/espacio_norma_2" android:src="@drawable/usuario" /> <TextView android:id="@+id/texto_nombre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginBottom="32dp" android:text="James Revelo" android:textAppearance="?android:attr/textAppearanceSmall" /> <ImageView android:id="@+id/imageView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginRight="8dp" android:src="@drawable/email" /> <TextView android:id="@+id/texto_email" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="james@mail.com" android:textAppearance="?android:attr/textAppearanceSmall" /> </GridLayout> </android.support.v7.widget.CardView> <TextView android:id="@+id/titulo_contrasena" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="@dimen/espacio_norma_2" android:paddingTop="@dimen/espacio_norma_1" android:text="@string/etiqueta_contrasena" android:textAppearance="?android:attr/textAppearanceSmall" /> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" card_view:cardCornerRadius="2dp" card_view:cardUseCompatPadding="true"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="@dimen/espacio_norma_1"> <ImageView android:id="@+id/icono_password" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_gravity="center" android:layout_marginRight="8dp" android:src="@drawable/contrasena" /> <TextView android:id="@+id/texto_password" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_gravity="center_vertical" android:layout_toRightOf="@+id/icono_password" android:text="@string/texto_cambiar_contrasena" android:textAppearance="?android:attr/textAppearanceSmall" /> <ImageView android:id="@+id/icono_indicador_derecho" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@drawable/indicador_derecho" /> </RelativeLayout> </android.support.v7.widget.CardView> </LinearLayout>
En el momento en que añadamos las pestañas este fragmento luciría de la siguiente forma:
5. Similar al paso anterior, esta vez diseñaremos el fragmento para las direcciones basándonos en el boceto.
Debido a que es una lista, necesitamos crear un layout para el fragmento con un recycler view. Algo tan sencillo como:
fragmento_grupo_items.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/reciclador" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" />
El layout de los ítems no es tan complicado. Solo son una serie de cuatro textos verticales con los datos de la dirección, el departamento, ciudad y teléfono.
Adicionalmente se añade un icono para decirle al usuario que cada ítem es editable.
item_lista_direcciones.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/espacio_norma_1"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/texto_direccion" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Dirección" android:textAppearance="?android:attr/textAppearanceSmall" android:textStyle="bold" /> <TextView android:id="@+id/texto_departamento" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Departamento" android:textAppearance="?android:attr/textAppearanceSmall" /> <TextView android:id="@+id/texto_ciudad" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Ciudad" android:textAppearance="?android:attr/textAppearanceSmall" /> <TextView android:id="@+id/texto_telefono" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Teléfono" android:textAppearance="?android:attr/textAppearanceSmall" /> </LinearLayout> <ImageView android:id="@+id/icono_indicador_derecho" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@drawable/indicador_derecho" /> </RelativeLayout>
Lo siguiente es crear el adaptador para las direcciones. Debido a que este fragmento es muy sencillo, podemos incluir la fuente de datos como clase interna.
AdaptadorDirecciones.java
import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.herprogramacion.restaurantericoparico.R; import java.util.ArrayList; import java.util.List; /** * Adaptador para poblar la lista de direcciones de la sección "Mi Cuenta" */ public class AdaptadorDirecciones extends RecyclerView.Adapter<AdaptadorDirecciones.ViewHolder> { public static class ViewHolder extends RecyclerView.ViewHolder { // Campos respectivos de un item public TextView direccion; public TextView departamento; public TextView ciudad; public TextView telefono; public ViewHolder(View v) { super(v); direccion = (TextView) v.findViewById(R.id.texto_direccion); departamento = (TextView) v.findViewById(R.id.texto_departamento); ciudad = (TextView) v.findViewById(R.id.texto_ciudad); telefono = (TextView) v.findViewById(R.id.texto_telefono); } } public AdaptadorDirecciones() { } @Override public int getItemCount() { return Direccion.DIRECCIONES.size(); } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.item_lista_direccion, viewGroup, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { Direccion item = Direccion.DIRECCIONES.get(i); viewHolder.direccion.setText(item.numeroDireccion); viewHolder.departamento.setText(item.departamento); viewHolder.ciudad.setText(item.ciudad); viewHolder.telefono.setText(item.telefono); } /** * Modelo de datos para probar el adaptador */ public static class Direccion { public String numeroDireccion; public String departamento; public String ciudad; public String telefono; public Direccion(String numeroDireccion, String departamento, String ciudad, String telefono) { this.numeroDireccion = numeroDireccion; this.departamento = departamento; this.ciudad = ciudad; this.telefono = telefono; } public final static List<Direccion> DIRECCIONES = new ArrayList<Direccion>(); static { DIRECCIONES.add(new Direccion("Cra 24 #2C-50", "Valle", "Cali", "3459821")); DIRECCIONES.add(new Direccion("Calle 100 Trans. 23", "Valle", "Cali", "4992600")); DIRECCIONES.add(new Direccion("Ave. 3ra N. #20-10", "Valle", "Cali", "4400725")); } } }
Un detalle adicional. Los recycler views se pueden estilizar con la subclase RecyclerView.ItemDecoration
. Esta permite recrear la forma en que son dibujados los elementos de la lista.
El boceto mostraba que usaríamos líneas divisorias para diferenciar los elementos, por lo que crearemos una decoración sencilla para este caso.
En primer lugar crea un nuevo drawable XML con un figura linear como se muestra en la siguiente definición.
linea_divisoria.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <size android:width="1dp" android:height="1dp" /> <solid android:color="@android:color/darker_gray" /> </shape>
Ahora crea una nueva clase que extienda de ItemDecoration
y modifica su método onDrawOver()
para renderizar el drawable desde el extremo izquierdo hacia el derecho:
DecoracionLineaDivisoria.java
import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; import android.view.View; import com.herprogramacion.restaurantericoparico.R; /** * ItemDecoration personalizado para dibujar la linea drawable/linea_divisoria.xml en los * elementos de un recycler view */ public class DecoracionLineaDivisoria extends RecyclerView.ItemDecoration { private Drawable lineaDivisoria; public DecoracionLineaDivisoria(Context context) { lineaDivisoria = ContextCompat.getDrawable(context, R.drawable.linea_divisoria); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int top = child.getBottom() + params.bottomMargin; int bottom = top + lineaDivisoria.getIntrinsicHeight(); lineaDivisoria.setBounds(left, top, right, bottom); lineaDivisoria.draw(c); } } }
Finalmente dentro de FragmentoDirecciones
recupera el recycler view y añade un nuevo adaptador junto a la decoración.
FragmentoDirecciones.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; /** * Fragmento para la pestaña "DIRECCIONES" De la sección "Mi Cuenta" */ public class FragmentoDirecciones extends Fragment { private LinearLayoutManager linearLayout; public FragmentoDirecciones() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragmento_grupo_items, container, false); RecyclerView reciclador = (RecyclerView)view.findViewById(R.id.reciclador); linearLayout = new LinearLayoutManager(getActivity()); reciclador.setLayoutManager(linearLayout); AdaptadorDirecciones adaptador = new AdaptadorDirecciones(); reciclador.setAdapter(adaptador); reciclador.addItemDecoration(new DecoracionLineaDivisoria(getActivity())); return view; } }
Al momento de coordinar las pestañas con las páginas el resultado para este fragmento debería ser el siguiente:
6. Ahora es el turno del fragmento de la sección TARJETAS. Se supone que este contiene la lista de las tarjetas que ha usado el usuario para adquirir comidas a domicilio. Sin embargo en este caso asumiremos que no existe ninguna, por lo que el boceto mostrará un mensaje asociado a la situación.
Este diseño es el más simple. Solo añade un nuevo fragmento que infle el layout de un ImageView
+ un TextView
(o un text view compuesto) y listo.
FragmentoTarjetas.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; /** * Fragmento para la pestaña "TARJETAS" de la sección "Mi Cuenta" */ public class FragmentoTarjetas extends Fragment { public FragmentoTarjetas() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragmento_tarjetas, container, false); } }
El diseño sería el siguiente:
fragmento_tarjetas.xml
<?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:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/imagen_no_tarjeta" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center_horizontal" android:src="@drawable/tarjeta_credito" /> <TextView android:id="@+id/texto_no_tarjeta" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="@string/texto_no_tarjetas" android:textAppearance="?android:attr/textAppearanceSmall" /> </LinearLayout>
Este pequeño código produciría el siguiente aspecto del fragmento:
7. Finalmente abre FragmentoCuenta.java y obtén una instancia y:
- Obtén una instancia del ViewPager del fragmento.
- Crea un FragmentStatePagerAdapter para poblar el pager.
- Añade una instancia de cada fragmento creado anteriormente.
- Usa el método
setupWithViewPager()
para coordinar las pestañas y páginas.
FragmentoCuenta.java
import android.graphics.Color; import android.os.Bundle; import android.support.design.widget.AppBarLayout; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; import java.util.ArrayList; import java.util.List; /** * Fragmento de la sección "Mi Cuenta" */ public class FragmentoCuenta extends Fragment { private AppBarLayout appBar; private TabLayout pestanas; private ViewPager viewPager; public FragmentoCuenta() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragmento_paginado, container, false); if (savedInstanceState == null) { insertarTabs(container); // Setear adaptador al viewpager. viewPager = (ViewPager) view.findViewById(R.id.view_pager_mi_cuenta); poblarViewPager(viewPager); pestanas.setupWithViewPager(viewPager); } return view; } private void insertarTabs(ViewGroup container) { View padre = (View) container.getParent(); appBar = (AppBarLayout) padre.findViewById(R.id.appbar); pestanas = new TabLayout(getActivity()); pestanas.setTabTextColors(Color.parseColor("#FFFFFF"), Color.parseColor("#FFFFFF")); appBar.addView(pestanas); } private void poblarViewPager(ViewPager viewPager) { AdaptadorSecciones adapter = new AdaptadorSecciones(getFragmentManager()); adapter.addFragment(new FragmentoPerfil(), getString(R.string.titulo_tab_perfil)); adapter.addFragment(new FragmentoDirecciones(), getString(R.string.titulo_tab_direcciones)); adapter.addFragment(new FragmentoTarjetas(), getString(R.string.titulo_tab_tarjetas)); viewPager.setAdapter(adapter); } @Override public void onDestroyView() { super.onDestroyView(); appBar.removeView(pestanas); } /** * Un {@link FragmentStatePagerAdapter} que gestiona las secciones, fragmentos y * títulos de las pestañas */ public class AdaptadorSecciones extends FragmentStatePagerAdapter { private final List<Fragment> fragmentos = new ArrayList<>(); private final List<String> titulosFragmentos = new ArrayList<>(); public AdaptadorSecciones(FragmentManager fm) { super(fm); } @Override public android.support.v4.app.Fragment getItem(int position) { return fragmentos.get(position); } @Override public int getCount() { return fragmentos.size(); } public void addFragment(android.support.v4.app.Fragment fragment, String title) { fragmentos.add(fragment); titulosFragmentos.add(title); } @Override public CharSequence getPageTitle(int position) { return titulosFragmentos.get(position); } } }
Recuerda que si el contenido de los fragmentos asociados a las pestañas varía de continuamente es mejor usar la clase FragmentStatePagerAdapter
que FragmentPagerAdapter
.
El primero recicla las vistas cada vez que el ciclo de vida del fragmento acaba, lo que facilitará el traspaso entre las opciones del navigation drawer sin alterar el contenido.
Para finalizar esta sección ejecuta el proyecto desarrollado hasta el momento y comprueba el funcionamiento de la sección Mi cuenta.
#6. Tabs Con Grids En Fragmentos Similares
La sección Categorías es similar a Mi Cuenta, ya que tenemos tabs asociadas a un fragmento. Solo que esta vez el contenido se define por grids de comidas segmentadas en categorías.
Al contener las pestañas elementos uniformes, el desarrollo se nos facilita mucho, ya que solo necesitamos un adaptador que se alimente de una fuente de datos personalizada, pero con la misma estructura de información.
Para llevar a cabo esta implementación requeriremos:
- Un fragmento para la sección Categorías.
- Un fragmento genérico que represente el contenido de todas las categorías.
- Un layout para el diseño de los ítems pertenecientes a los grids.
- Un adaptador para los ítems en los grids.
Pasemos a la acción…
1. Crea un nuevo fragmento llamado FragmentoCategorias.java e infla su contenido desde el layout existente fragmento_paginado.xml.
2. Copia y pega exactamente el mismo código del FragmentoCuenta.java en su interior excepto el cuerpo del método poblarViewPager()
. Recuerda que usaremos fragmentos distintos para la creación de los grids por cada sección.
3. Por segunda vez añade un fragmento nuevo y nómbralo FragmentoCategoria.java. Debido a que usaremos un recycler view para generar las grillas, podemos reutilizar el layout fragmento_grupo_items.xml para inflar este fragmento.
4. Añade a la clase Comida.java tres nuevas listas para representar los ítems proyectados en las grillas. Sus nombres serán PLATILLOS
, BEBIDAS
y POSTRES
como en los títulos de las pestañas.
Dentro de Comida.java
public static final List<Comida> BEBIDAS = new ArrayList<>(); public static final List<Comida> POSTRES = new ArrayList<>(); public static final List<Comida> PLATILLOS = new ArrayList<>(); static { ... PLATILLOS.add(new Comida(5, "Camarones Tismados", R.drawable.camarones)); PLATILLOS.add(new Comida(3.2f, "Rosca Herbárea", R.drawable.rosca)); PLATILLOS.add(new Comida(12f, "Sushi Extremo", R.drawable.sushi)); PLATILLOS.add(new Comida(9, "Sandwich Deli", R.drawable.sandwich)); PLATILLOS.add(new Comida(34f, "Lomo De Cerdo Austral", R.drawable.lomo_cerdo)); BEBIDAS.add(new Comida(3, "Taza de Café", R.drawable.cafe)); BEBIDAS.add(new Comida(12, "Coctel Tronchatoro", R.drawable.coctel)); BEBIDAS.add(new Comida(5, "Jugo Natural", R.drawable.jugo_natural)); BEBIDAS.add(new Comida(24, "Coctel Jordano", R.drawable.coctel_jordano)); BEBIDAS.add(new Comida(30, "Botella Vino Tinto Darius", R.drawable.vino_tinto)); POSTRES.add(new Comida(2, "Postre De Vainilla", R.drawable.postre_vainilla)); POSTRES.add(new Comida(3, "Flan Celestial", R.drawable.flan_celestial)); POSTRES.add(new Comida(2.5f, "Cupcake Festival", R.drawable.cupcakes_festival)); POSTRES.add(new Comida(4, "Pastel De Fresa", R.drawable.pastel_fresa)); POSTRES.add(new Comida(5, "Muffin Amoroso", R.drawable.muffin_amoroso)); }
5. Lo siguiente es crear un diseño para los ítems del grid. En este caso usaremos la modalidad footer con doble línea como se muestra en la documentación y en el boceto.
El layout se compone de la imagen superior de cada comida con una altura predeterminada a 192dp. El footer será un LinearLayout
con dos Text Views, donde la primer línea será el precio y la segunda el nombre de la comida.
item_lista_categorias.xml
<?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:orientation="vertical" android:padding="1dp"> <ImageView android:id="@+id/miniatura_comida" android:layout_width="match_parent" android:layout_height="192dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/accentColor" android:orientation="vertical" android:padding="@dimen/espacio_norma_1"> <TextView android:id="@+id/precio_comida" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Precio" android:textAppearance="@style/TextAppearance.AppCompat.Subhead.Inverse" android:textStyle="normal|bold" /> <TextView android:id="@+id/nombre_comida" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nombre" android:textAppearance="@style/TextAppearance.AppCompat.Subhead.Inverse" android:textSize="12sp" /> </LinearLayout> </LinearLayout>
Más adelante este diseño produciría el siguiente aspecto visual:
6. Crea un adaptador para los elementos que permita recibir como parámetro una lista de ítems genéricos. Esto con el fin de crear las tres variaciones de las categorías.
AdaptadorCategorias.java
import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.herprogramacion.restaurantericoparico.R; import com.herprogramacion.restaurantericoparico.modelo.Comida; import java.util.List; /** * Adaptador para comidas usadas en la sección "Categorías" */ public class AdaptadorCategorias extends RecyclerView.Adapter<AdaptadorCategorias.ViewHolder> { private final List<Comida> items; public static class ViewHolder extends RecyclerView.ViewHolder { // Campos respectivos de un item public TextView nombre; public TextView precio; public ImageView imagen; public ViewHolder(View v) { super(v); nombre = (TextView) v.findViewById(R.id.nombre_comida); precio = (TextView) v.findViewById(R.id.precio_comida); imagen = (ImageView) v.findViewById(R.id.miniatura_comida); } } public AdaptadorCategorias(List<Comida> items) { this.items = items; } @Override public int getItemCount() { return items.size(); } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.item_lista_categorias, viewGroup, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { Comida item = items.get(i); Glide.with(viewHolder.itemView.getContext()) .load(item.getIdDrawable()) .centerCrop() .into(viewHolder.imagen); viewHolder.nombre.setText(item.getNombre()); viewHolder.precio.setText("$" + item.getPrecio()); } }
7. Abre de nuevo FragmentoCategoria.java y añade el siguiente código.
FragmentoCategoria.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.restaurantericoparico.R; import com.herprogramacion.restaurantericoparico.modelo.Comida; /** * Fragmento que representa el contenido de cada pestaña dentro de la sección "Categorías" */ public class FragmentoCategoria extends Fragment { private static final String INDICE_SECCION = "com.restaurantericoparico.FragmentoCategoriasTab.extra.INDICE_SECCION"; private RecyclerView reciclador; private GridLayoutManager layoutManager; private AdaptadorCategorias adaptador; public static FragmentoCategoria nuevaInstancia(int indiceSeccion) { FragmentoCategoria fragment = new FragmentoCategoria(); Bundle args = new Bundle(); args.putInt(INDICE_SECCION, indiceSeccion); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragmento_grupo_items, container, false); reciclador = (RecyclerView) view.findViewById(R.id.reciclador); layoutManager = new GridLayoutManager(getActivity(), 2); reciclador.setLayoutManager(layoutManager); int indiceSeccion = getArguments().getInt(INDICE_SECCION); switch (indiceSeccion) { case 0: adaptador = new AdaptadorCategorias(Comida.PLATILLOS); break; case 1: adaptador = new AdaptadorCategorias(Comida.BEBIDAS); break; case 2: adaptador = new AdaptadorCategorias(Comida.POSTRES); break; } reciclador.setAdapter(adaptador); return view; } }
Las piezas de código tienen como fin la personalización del recycler view dependiendo de la categoría.
Al principio encontrarás la clave de un extra que representa el índice de la pestaña para la cual se creará el fragmento.
El método de clase nuevaInstancia()
permite crear una nueva instancia del fragmento basado en el índice de la sección como argumento.
Dicho argumento es obtenido en el método onCreateView()
para determinar con un switch
que fuente de datos irá en el constructor del adaptador. Luego de la decisión, el adaptador es asignado al recycler view que se creó.
Recuerda que si deseas crear un Grid List con el RecyclerView
es necesario usar un GridLayoutManager
en la inicialización. El constructor de este elemento recibe el contexto y la cantidad de columnas que habrá (en este caso 2).
8. Finalmente puedes modificar el método poblarViewPager()
de FragmentoCategorias
añadiendo tres instancias con índices del 0 al 2 para satisfacer la estructura de las pestañas.
Dentro de FragmentoCategorias.java
private void poblarViewPager(ViewPager viewPager) { AdaptadorSecciones adapter = new AdaptadorSecciones(getFragmentManager()); adapter.addFragment(FragmentoCategoria.nuevaInstancia(0), getString(R.string.titulo_tab_platillos)); adapter.addFragment(FragmentoCategoria.nuevaInstancia(1), getString(R.string.titulo_tab_bebidas)); adapter.addFragment(FragmentoCategoria.nuevaInstancia(2), getString(R.string.titulo_tab_postres)); viewPager.setAdapter(adapter); }
Si deseas añadir un action button de búsqueda dentro de la Toolbar
, entonces crea el siguiente recurso de menú.
menu_categorias.xml
<menu 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" tools:context=".ActividadPrincipal"> <item android:id="@+id/action_settings" android:orderInCategory="100" android:title="@string/action_settings" app:showAsAction="never" /> <item android:id="@+id/action_search" android:icon="@drawable/busqueda" android:orderInCategory="1" android:title="@string/action_search" app:showAsAction="ifRoom" /> </menu>
Y ahora habilita la contribución del fragmento a la action bar con el método setHasOptionsMenu()
dentro de onCreate()
y luego infla el recurso dentro de onCreateOptionsMenu()
.
Dentro de FragmentoCategorias.java
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_categorias, menu); }
Si ejecutas la aplicación tendrás el siguiente resultado:
#7. Iniciar Actividad Desde Navigation Drawer
Ya para finalizar nuestro experimento veremos cómo iniciar una actividad de ajustes desde el navigation drawer. Según el boceto Rico Pa’ Rico tiene una sola preferencia relacionada con la configuración de la frecuencia de notificaciones por correo. Esta a su vez se encuentra dentro de la categoría «Notificaciones».
A simple vista se reconoce que esta preferencia es del tipo ListPreference
como vimos en el artículo ¿Cómo Crear Una Actividad De Preferencias En Android?. Donde se selecciona una de las opciones mostradas en un dialogo para tomar una decisión dentro de la aplicación.
La creación de esta actividad solamente requiere:
- Una actividad con Toolbar junto a su respectivo layout.
- Un fragmento de preferencias.
- Un archivo strings.xml para preferencias.
- Un recurso xml para mapear las preferencias que se inflarán en el fragmento.
Continuemos…
1. Crea una nueva actividad llamada ActividadConfiguracion.java junto a un layout que contenga una Toolbar y un contenido central.
actividad_configuracion.xml
<LinearLayout 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="match_parent" android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> <FrameLayout android:id="@+id/contenedor_configuracion" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>
2. Dentro de la actividad añade un fragmento como miembro y extiéndelo de la clase PreferenceFragment
. Solo necesitamos que en su método onCreate()
se inflen las preferencias a través de addPreferencesFromResource()
.
ActividadConfiguracion.java
import android.app.FragmentTransaction; import android.os.Bundle; import android.preference.PreferenceFragment; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import com.herprogramacion.restaurantericoparico.R; /** * Actividad para la configuración de preferencias */ public class ActividadConfiguracion extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.actividad_configuracion); FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.add(R.id.contenedor_configuracion, new FragmentoConfiguracion()); ft.commit(); agregarToolbar(); } private void agregarToolbar() { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); final ActionBar ab = getSupportActionBar(); if (ab != null) { ab.setDisplayHomeAsUpEnabled(true); } } public static class FragmentoConfiguracion extends PreferenceFragment { public FragmentoConfiguracion() { // Constructor Por Defecto } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferencias); } } }
3. Ve a res/values y crea una nuevo recursos de strings con el nombre de strings_actividad_configuracion.xml y añade los textos necesarios que se ven en el boceto para proyectar la preferencia.
strings_actividad_configuracion.xml
<resources> <string name="titulo_actividad_configuracion">Configuración</string> <string name="categoria_notificaciones">Notificaciones</string> <string name="titulo_preferencia">Frecuencia de notificaciones por correo</string> <string name="key_preferencia">lista_frecuencias</string> <string-array name="lista_frecuencia"> <item>Un correo diario</item> <item>Un correo a la semana</item> <item>Un correo al mes</item> <item>No quiero recibir correos</item> </string-array> <string-array name="lista_valores_frecuencia"> <item>0</item> <item>1</item> <item>2</item> <item>3</item> </string-array> </resources>
El recurso anterior contiene el nombre de la actividad de preferencias, el nombre de la categoría. También el título de la preferencia de lista, su clave (key
) y dos arrays para representar el texto de las opciones y sus valores correspondientes.
4. Crea un nuevo recurso dentro de la carpeta xml (si no existe, créala) con un nodo PreferenceScreen
. Luego añade una categoría junto a la ListPreference
que ya veníamos planeando.
preferencias.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:key="categoria_notificaciones" android:title="@string/categoria_notificaciones"> <ListPreference android:defaultValue="0" android:entries="@array/lista_frecuencia" android:entryValues="@array/lista_valores_frecuencia" android:key="@string/key_preferencia" android:negativeButtonText="@null" android:positiveButtonText="@null" android:summary="%s" android:title="@string/titulo_preferencia" /> </PreferenceCategory> </PreferenceScreen>
5. Modifica el estilo de la actividad principal en el Android Manifest a @style/Theme.ConNavigationDrawer
. Esto evitará que la status bar se vea translucida debido al efecto que se usa en la actividad principal para seguir las especificaciones del navigation drawer.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.herprogramacion.restaurantericoparico" > <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.RicoPaRico" > <activity android:name=".ui.ActividadPrincipal" android:label="@string/app_name" android:theme="@style/Theme.ConNavigationDrawer"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ui.ActividadConfiguracion" android:label="@string/titulo_actividad_configuracion" android:parentActivityName=".ui.ActividadPrincipal" > <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".ui.ActividadPrincipal" /> </activity> </application> </manifest>
6. Dentro de ActividadPrincipal.java usa el método startActivity()
para iniciar nuestra actividad de configuración. Recuerda que esto va sobre el método seleccionarItem()
y el identificador de la sección de configuración es R.id.item_configuracion
.
Dentro de ActividadPrincipal.java
private void seleccionarItem(MenuItem itemDrawer) { Fragment fragmentoGenerico = null; FragmentManager fragmentManager = getSupportFragmentManager(); switch (itemDrawer.getItemId()) { case R.id.item_inicio: fragmentoGenerico = new FragmentoInicio(); break; case R.id.item_cuenta: fragmentoGenerico = new FragmentoCuenta(); break; case R.id.item_categorias: fragmentoGenerico = new FragmentoCategorias(); break; case R.id.item_configuracion: startActivity(new Intent(this, ActividadConfiguracion.class)); break; } if (fragmentoGenerico != null) { fragmentManager .beginTransaction() .replace(R.id.contenedor_principal, fragmentoGenerico) .commit(); } // Setear título actual setTitle(itemDrawer.getTitle()); }
Si todo salió bien, cuando ejecutes la aplicación y selecciones la sección de configuración, se iniciará nuestra actividad y podremos variar el valor de la preferencia de notificaciones.
Conclusión
El patrón recomendado por Google para usar dos niveles de navegación es el de Navigation Drawer y Tabs.
Este componente brinda flexibilidad y organización a la hora de crear secciones que ayuden al usuario a tener una visión global de la aplicación.
Vimos cómo usar varios tipos de layouts para generar un segundo nivel de navegación,basados en elemento como listas, grid views y pestañas.
La aplicación Rico Pa’ Rico permitió usar diferentes fragmentos para las secciones del navigation drawer. Esta vez nuestro desarrollo fue dinámico gracias a los fragmentos anidados.
Como se mencionó arriba, también es posible crear una navigation drawer con diferentes actividades. Pero este es un tema que será tratado individualmente.
Por ahora puedes ir pensando en alimentar esta plantilla con datos reales a través de un web service y sincronizar los datos locales con tu servidor.