Si necesitas aprovechar el espacio sobrante que dejan tus listas en una tablet, este tutorial es para ti. Donde aprenderás a emplear el patrón master-detail en dispositivos con dimensiones amplias.
Para ello te explicaré los pasos necesarios para generar una aplicación Android de ejemplo llamada «Tips de salud», la cual contiene un fragmento con un RecyclerView
, cuyo contenido son artículos con hábitos de vida sana.
Por cada elemento de la lista tendremos un detalle que será extendido en la misma actividad si se trata de tablets (dispositivos con más de 900dp) o se iniciará una nueva actividad de detalle con dicha información (teléfonos).
Descargar Proyecto Android Studio De «Tips De Salud»
A continuación te dejo el resultado final en un video del proyecto en Android Studio que terminarás creando:
Para desbloquear el link de descarga sigue estas instrucciones:
[sociallocker id=»7121″]
Patrón Master-Detail En Tablets
El mecanismo master-detail hace referencia a un escenario de interfaz de usuario donde se posee una lista de elementos homogéneos y se permite obtener una vista detallada del elemento en una porción más extensa de UI.
En teléfonos tendremos la capacidad de acomodar un solo panel para esta interacción. Donde al seleccionar un ítem de la lista la app te dirigirá a otra pantalla con el detalle:
Por otro lado, las tablets debido a sus amplias dimensiones permiten usar ambos paneles en conjunto. Donde la selección de un ítem, actualiza el panel de detalle en tiempo real:
A esto se le llama una planeación para múltiples tamaños de pantallas al momento de abordar la estructura de una aplicación Android. Este enfoque nos evita caer en un pobre uso del espacio y la distribución excesiva de los elementos.
Un buen ejemplo para obtener ideas de inspiración es la aplicación móvil de Evernote, la cual muestra la lista de marcadores guardados y contiguamente se encuentra el flujo de su detalle.
Ahora la gran pregunta es: ¿Cómo implementaré este patrón master-detail en mis apps?
Si tu intuición es aguda, ya sabrás que el uso de secciones de UI dentro de una actividad se maneja con fragmentos. Estas ‘subactividades’ te permiten proporcionar un ciclo de vida a una porción de interfaz de usuario con el fin de reducir complejidad y añadir personalización a tus apps.
La idea de este tutorial es mostrarte el camino para crear un layout con dos paneles en una tablet y condicionar a que solo sea una actividad de detalle cuando se trate de teléfonos. La siguiente ilustración muestra el esquema general:
Para ello necesitamos:
- Crear actividad de lista
- Crear fragmento de lista
- Crear un POJO para los artículos
- Crear un adaptador para la lista
- Crear actividad del detalle
- Crear fragmento del detalle
Todos esos elementos los crearás basado en el siguiente modelo final de la aplicación «Tips de salud»:
Como ves, el panel izquierdo contiene una lista de artículos de salud y el panel derecho contiene el detalle de un elemento seleccionado. Veamos cómo abordar este diseño.
1. Configurar Un Nuevo Proyecto En Android Studio
Paso 1. Como siempre, abre Android Studio y crea un nuevo proyecto llamado «Tips De Salud». Cuando el asistente te permita elegir la actividad principal que usarás, selecciona el tipo «Blank Activity» y luego llámala ActividadListaArticulos.java. Además cambia su layout por actividad_lista_articulos.xml.
Para teléfonos modifica el layout original por el siguiente:
actividad_lista_articulos.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:fitsSystemWindows="true"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:popupTheme="@style/AppTheme.PopupOverlay"> </android.support.v7.widget.Toolbar> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/contenedor_lista" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </android.support.design.widget.CoordinatorLayout>
En cuanto a tablets es necesario crear la variación de este layout pero en la carpeta values-w900dp. En esencia es el mismo layout, solo que añadiremos el segundo panel del detalle, el cual debe superponerse a la app bar principal.
Además agregaremos un ImageView
para simular el icono de búsqueda en la app bar principal.
actividad_lista_articulos.xml (w900dp)
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:fitsSystemWindows="true"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:gravity="center_vertical" app:popupTheme="@style/AppTheme.PopupOverlay"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="320dp" android:layout_marginStart="320dp" android:src="@drawable/icono_busqueda" /> </android.support.v7.widget.Toolbar> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/contenedor_lista" android:layout_width="@dimen/ancho_panel_lista" android:layout_height="match_parent" android:layout_marginTop="?attr/actionBarSize" android:background="@android:color/white" /> <FrameLayout android:id="@+id/contenedor_detalle_articulo" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="@dimen/ancho_panel_lista" android:layout_toRightOf="@+id/contenedor_lista" android:background="@android:color/white" android:elevation="8dp" /> </android.support.design.widget.CoordinatorLayout>
Paso 2. Ve a la aplicación Material Design Colors y selecciona un esquema principal con los colores Rosa 700, Rosa 400, Rosa 100. Para el acento usa el color Cyan 400.
Ahora abre tu archivo colors.xml y establece los colores en generados en los ítems colorPrimary
, colorPrimaryDark
y colorAccent
.
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#EC407A</color> <color name="colorPrimaryDark">#C2185B</color> <color name="colorAccent">#26C6DA</color> </resources>
Paso 3. Abre tú archivo build.gradle del módulo principal y agrega las siguientes librerías:
build.gradle
dependencies { ... compile 'com.android.support:recyclerview-v7:23.1.1' compile 'com.github.bumptech.glide:glide:3.6.1' }
La primera línea se trata de la librería para crear listas con el RecyclerView. La segunda es Glide para la carga de imágenes desde urls con caching integrado. Asegúrate de añadir un permiso de conexión a internet dentro del AndroidManifest.xml para acceder a las imágenes.
<uses-permission android:name="android.permission.INTERNET"/>
Paso 4. Para terminar la configuración general de los aspectos del proyecto, abre el archivo de dimensiones y agrega las siguientes medidas dentro de él.
dimens.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="margen_estandar">16dp</dimen> <dimen name="altura_item">122dp</dimen> <dimen name="margen_izquierda_contenido">72dp</dimen> <dimen name="ancho_miniatura">120dp</dimen> <dimen name="ancho_panel_lista">512dp</dimen> </resources>
Los significados vienen así:
- La margen estándar hace referencia al espaciado que se usa con mayor frecuencia en Android.
- La altura del ítem es el tamaño de cada ítem de la lista.
- La margen izquierda del contenido se usa como alineación en el detalle.
- El ancho de la miniatura hace referencia al tamaño de la imagen en los ítems de la lista.
- El ancho del panel de lista hace referencia a su tamaño.
2. Crear Fragmento Con RecyclerView
Paso 1. El siguiente movimiento es crear el fragmento que contendrá la lista basada en un Recycler View. Esto solo requiere que vayas a File > New >Fragment > Fragment(Blank) con las siguientes características.
Paso 2. Abre el layout del fragmento y añade un nodo <RecyclerView>
para representar la lista. Puedes encerrar la lista dentro de un contenedor por si deseas personalizar mucho más el fragmento.
fragmento_lista_articulos.xml
<FrameLayout 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" tools:context="com.herprogramacion.tipsdesalud.FragmentoListaArticulos"> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/reciclador" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" tools:listitem="@layout/item_lista_articulos" /> </FrameLayout>
Si eres buen observador verás que he incrustado el layout manager a través del atributo app:layoutManager
para generar una lista. Incluso también podemos visualizar el diseño de los ítems en la vista de previsualización con el atributo auxiliar tools:listitem
.
Paso 3. En el paso anterior viste la referencia al layout de los ítems llamado item_lista_articulos.xml, pero como aún no lo tienes creado, entonces añade un nuevo layout e intenta replicar el siguiente diseño.
Es muy sencillo. Me pareció más cómodo usar un Relative Layout para ubicar los elementos con referencia al padre. Sin embargo un Grid Layout también vendría bien.
Observa:
item_lista_articulos.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="@dimen/altura_item" android:orientation="horizontal" android:layout_marginTop="@dimen/margen_estandar" android:layout_marginBottom="@dimen/margen_estandar"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Título" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:id="@+id/titulo" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/miniatura" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Resumen" android:id="@+id/resumen" android:layout_below="@+id/titulo" android:layout_alignLeft="@+id/titulo" android:layout_alignRight="@+id/titulo" android:layout_above="@+id/fecha" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Fecha" android:id="@+id/fecha" android:layout_alignLeft="@+id/resumen" android:layout_alignRight="@+id/titulo" android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:layout_alignParentBottom="true" /> <ImageView android:layout_width="@dimen/ancho_miniatura" android:layout_height="match_parent" android:id="@+id/miniatura" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/margen_estandar" /> </RelativeLayout>
3. Crear POJO Para Los Artículos
Ya sabes que la lista requiere una fuente de datos para alimentar a sus views. Por motivos de agilidad e ilustración crearé un modelo de datos que refleje someramente el contenido de un artículo.
Sin embargo, si es necesario, detente en este lugar para pensar la creación de tu propia base de datos SQLite; el parsing de elementos JSON o el parsing XML.
Paso 1. La vista de la sección anterior te muestra una gran cantidad de atributos que requiere el modelo de datos:
- título
- resumen|contenido
- fecha
- miniatura|imagen principal
Esto haría ver la clase POJO de la siguiente forma:
/** * Un 'articulo' representa la estructura general de cada tip de salud */ public static class Articulo { public final String id; public final String titulo; public final String descripcion; public final String fecha; public final String urlMiniatura; public Articulo(String id, String titulo, String descripcion, String fecha, String urlMiniatura) { this.id = id; this.titulo = titulo; this.descripcion = descripcion; this.fecha = fecha; this.urlMiniatura = urlMiniatura; } }
Paso 2. Ahora crea un nuevo paquete Java y agrega una clase llamada ModeloArticulo.java. Luego añade como miembro a la clase Articulo
. El objetivo de esta envoltura es representar una fuente de datos que entrega grandes bloques de datos y además de ello permite realizar una búsqueda entre todos ellos.
ModeloArticulo.java
import android.support.annotation.NonNull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; /** * Modelo de datos para los artículos que se inflarán en la lista */ public class ModeloArticulos { /** * Arreglo de objetos {@link Articulo} que simula una fuente de datos */ public static final List<Articulo> ITEMS = new ArrayList<Articulo>(); /** * Mapa simulador de búsquedas de articulos por id */ public static final Map<String, Articulo> MAPA_ITEMS = new HashMap<String, Articulo>(); static { // Añadir elementos de ejemplo agregarItem(new Articulo(generarId(), "10 Plantillas Para Determinar Tus Necesidades Calóricas", "Descarga nuestras plantillas para Microsoft Excel que te permitirán, calcular exactamente...", "10 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo1.jpg")); agregarItem(new Articulo(generarId(), "¿Qué Tan Malo Es Consumir Chocolate?", "Si aún no has podido resolver tus dudas sobre comer chocolate, entonces este artículo es para ti...", "11 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo2.jpg")); agregarItem(new Articulo(generarId(), "Guía Para Identificar Alimentos Con Trigo Perjudiciales", "Aprende a distinguir aquellos alimentos que están hechos de trigo mal procesado...", "12 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo3.jpg")); agregarItem(new Articulo(generarId(), "Descubre Qué Harían 10 Minutos De Ejercicio Diario En Tu Cuerpo", "Entra y descubre los beneficios de realizar una rutina de 10 minutos los 7 días...", "13 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo4.jpg")); agregarItem(new Articulo(generarId(), "Aumentando Las Defensas Del Cuerpo Con Frutos Rojos", "Los frutos rojos frecuentemente son ignorados por la mayoría de las personas, pero hoy te mostraremos...", "14 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo5.jpg")); agregarItem(new Articulo(generarId(), "5 Recetas Bajas En Grasa Para El Almuerzo", "Variar las recetas a la hora de alimentarse permite obtener distintos beneficios dependiendo del alimento, ...", "15 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo6.jpg")); agregarItem(new Articulo(generarId(), "Combina Cardio + Fuerza Para Obtener Verdaderos Resultados", "Muchos creen que solo realizar ejercicios de cardio los hará perder de peso y...", "16 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo7.jpg")); agregarItem(new Articulo(generarId(), "Planificador Semanal Para Optimizar Tu Dieta", "Descarga nuestro nueva plantilla para prácticar buenos hábitos alimenticios. Se trata de...", "17 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo8.jpg")); agregarItem(new Articulo(generarId(), "Las 30 Razones De Por Qué Superman Come Cacahuates", "Los frutos secos han sido desde la antiguedad la principal fuente de proteínas para muchas civilizaciones...", "18 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo9.jpg")); } @NonNull private static String generarId() { return UUID.randomUUID().toString(); } private static void agregarItem(Articulo item) { ITEMS.add(item); MAPA_ITEMS.put(item.id, item); } /** * Un 'articulo' representa la estructura general de cada tip de salud */ public static class Articulo { public final String id; public final String titulo; public final String descripcion; public final String fecha; public final String urlMiniatura; public Articulo(String id, String titulo, String descripcion, String fecha, String urlMiniatura) { this.id = id; this.titulo = titulo; this.descripcion = descripcion; this.fecha = fecha; this.urlMiniatura = urlMiniatura; } } }
La lista de artículos estática ITEMS
actuará como origen de datos para el adaptador de la lista que se creará. El mapa MAPA_ITEMS
será el índice de búsqueda del cuál podrás obtener datos a través de un identificador.
4. Crear Adaptador Para La Lista
Una vez que tengas listo el modelo, ya es posible generar el adaptador. Para ello crea una nueva clase y nómbrala AdaptadorArticulos.java. Haz que extienda de RecyclerView.Adapter
y sobrescribe los controladores onCreateViewHolder()
, onBindViewHolder()
y getCount()
cómo ya has hecho infinidad de veces.
AdaptadorArticulos.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.tipsdesalud.modelo.ModeloArticulos; import java.util.List; /** * {@link RecyclerView.Adapter} que alimenta la lista con * instancias {@link ModeloArticulos.Articulo} */ public class AdaptadorArticulos extends RecyclerView.Adapter<AdaptadorArticulos.ViewHolder> { private final List<ModeloArticulos.Articulo> valores; public AdaptadorArticulos(List<ModeloArticulos.Articulo> items, OnItemClickListener escuchaClicksExterna) { valores = items; this.escuchaClicksExterna = escuchaClicksExterna; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_lista_articulos, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(final ViewHolder holder, int position) { holder.item = valores.get(position); holder.viewTitulo.setText(valores.get(position).titulo); holder.viewResumen.setText(valores.get(position).descripcion); holder.viewFecha.setText(valores.get(position).fecha); Glide.with(holder.itemView.getContext()) .load(holder.item.urlMiniatura) .thumbnail(0.1f) .centerCrop() .into(holder.viewMiniatura); } @Override public int getItemCount() { if (valores != null) { return valores.size() > 0 ? valores.size() : 0; } else { return 0; } } private String obtenerIdArticulo(int posicion) { if (posicion != RecyclerView.NO_POSITION) { return valores.get(posicion).id; } else { return null; } } public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public final TextView viewTitulo; public final TextView viewResumen; public final TextView viewFecha; public final ImageView viewMiniatura; public ModeloArticulos.Articulo item; public ViewHolder(View view) { super(view); view.setClickable(true); viewTitulo = (TextView) view.findViewById(R.id.titulo); viewResumen = (TextView) view.findViewById(R.id.resumen); viewFecha = (TextView) view.findViewById(R.id.fecha); viewMiniatura = (ImageView) view.findViewById(R.id.miniatura); view.setOnClickListener(this); } @Override public void onClick(View v) { escuchaClicksExterna.onClick(this, obtenerIdArticulo(getAdapterPosition())); } } public interface OnItemClickListener { public void onClick(ViewHolder viewHolder, String idArticulo); } private OnItemClickListener escuchaClicksExterna; }
Recuerda que la interfaz que añades llamada OnItemClickListener
(obviamente puedes usar otro nombre) es una interfaz que transmite los eventos de click al fragmento o actividad contenedora del recycler view.
El método obtenerIdArticulo()
retorna en el atributo idArticulo
de cada objeto Articulo
asociado a los ítems de la lista. Este permite enviar por el controlador onClick()
el identificador para determinar el detalle de cada artículo.
Punto de ejecución — Abre el fragmento FragmentoListaArticulos
y prepara el RecyclerView
de la siguiente forma:
import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos; /** * Fragmento especializado para la lista de artículos */ public class FragmentoListaArticulos extends Fragment implements AdaptadorArticulos.OnItemClickListener { private EscuchaFragmento escucha; public FragmentoListaArticulos() { } public static FragmentoListaArticulos crear() { return new FragmentoListaArticulos(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { // Manejo de argumentos } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragmento_lista_articulos, container, false); View recyclerView = v.findViewById(R.id.reciclador); assert recyclerView != null; prepararLista((RecyclerView) recyclerView); return v; } private void prepararLista(@NonNull RecyclerView recyclerView) { recyclerView.setAdapter(new AdaptadorArticulos(ModeloArticulos.ITEMS, this)); } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof EscuchaFragmento) { escucha = (EscuchaFragmento) context; } else { throw new RuntimeException(context.toString() + " debes implementar EscuchaFragmento"); } } @Override public void onDetach() { super.onDetach(); escucha = null; } @Override public void onClick(AdaptadorArticulos.ViewHolder viewHolder, String idArticulo) { } public interface EscuchaFragmento { void alSeleccionarItem(String idArticulo); } }
Obtén el recycler view en onCreateView()
y luego asignar su adaptador con prepararLista()
.
prepararLista()
recibe la lista estática ModeloArticulo.ITEMS
y al propio fragmento como escucha(recuerda implementar la interfaz del adaptador e implementar el controlador onClick()
.
Ahora ve a tu actividad de lista de artículos y agrega el fragmento en el método onCreate()
:
ActividadListaArticulos.java
import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos; /** * Actividad con la lista de artículos. Si el ancho del dispositivo es mayor o igual a 900dp, entonces * se incrusta el fragmento de detalle {@link FragmentoDetalleArticulo} para generar el patrón * Master-detail */ public class ActividadListaArticulos extends AppCompatActivity implements FragmentoListaArticulos.EscuchaFragmento { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.actividad_lista_articulos); ((Toolbar) findViewById(R.id.toolbar)).setTitle(getTitle()); // Agregar fragmento de lista getSupportFragmentManager() .beginTransaction() .replace(R.id.contenedor_lista, FragmentoListaArticulos.crear()) .commit(); } @Override public void alSeleccionarItem(String idArticulo) { } }
Es necesario implementar la interfaz del fragmento para que este sea inflado. Con esto parcialmente construido ya puedes ejecutar la app y obtener el siguiente resultado.
Hasta el momento ya tienes el primer panel de la aplicación. Ahora solo falta el detalle.
5. Crear Actividad De Detalle
Lo siguiente es crear la actividad para el detalle de los artículos que se mostrará en los teléfonos. Tan solo ve a File > New > Activity > Blank Activity. Usa el nombre de ActividadDetalleArticulo y denomina al layout cómo actividad_detalle_articulo.xml.
Por el momento solo actualizaremos el layout para que mantenga una toolbar simple junto a un FrameLayout
donde se incrustará el fragmento del detalle.
actividad_detalle_articulo.xml
<android.support.design.widget.CoordinatorLayout 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:fitsSystemWindows="true" tools:context=".ActividadDetalleArticulo" tools:ignore="MergeRootFrame"> <FrameLayout android:id="@+id/contenedor_detalle_articulo" android:layout_width="match_parent" android:layout_height="match_parent" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar_detalle" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@android:color/transparent" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CoordinatorLayout>
6. Crear Fragmento Para Detalle
El fragmento del detalle tendrá un diseño donde se encuentra una imagen de cabecera en la parte superior, la cual se contrasta con una Toolbar transparente. La siguiente sección es un view insignia donde va el título del artículo y la fecha de publicación. Finalmente en la tercera sección tendremos el contenido total del texto del artículo.
Paso 1. Para diseñar esta interfaz en teléfonos, primero crea un fragmento sin interfaz de comunicación llamado FragmentoDetalleArticulo.java junto a un layout llamado fragmento_detalle_articulo.xml.
Para crear el diseño puedes usar un LinearLayout
vertical, donde distribuyas las tres secciones normales. Puedes usar una proporción 50% y 50% entre la sección de la imagen para resaltar la cabecera.
fragmento_detalle_articulo.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/articulo_detail" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".FragmentoDetalleArticulo"> <ImageView android:id="@+id/imagen" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="50" android:scaleType="centerCrop" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?colorPrimary" android:elevation="2dp" android:orientation="vertical" android:paddingBottom="@dimen/margen_estandar" android:paddingEnd="@dimen/margen_estandar" android:paddingRight="@dimen/margen_estandar" android:paddingStart="@dimen/margen_izquierda_contenido" android:paddingLeft="@dimen/margen_izquierda_contenido" android:paddingTop="@dimen/margen_estandar"> <TextView android:id="@+id/titulo" style="@style/TextAppearance.AppCompat.Title.Inverse" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="2" android:textIsSelectable="true" /> <TextView android:id="@+id/fecha" style="@style/TextAppearance.AppCompat.Subhead.Inverse" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="50" android:clipToPadding="false" android:paddingBottom="@dimen/margen_estandar" android:paddingTop="@dimen/margen_estandar" android:scrollbarStyle="outsideOverlay"> <TextView android:id="@+id/contenido" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="@dimen/margen_izquierda_contenido" android:paddingStart="@dimen/margen_izquierda_contenido" android:paddingEnd="@dimen/margen_estandar" android:paddingRight="@dimen/margen_estandar" android:text="Contenido" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> </ScrollView> </LinearLayout>
Paso 2. El layout del fragmento para tablets es exactamente lo mismo, solo que se requiere una Toolbar con respecto a la imagen de cabecera.
Una de las formas más sencillas de conseguir este efecto es envolver la imagen y la Toolbar dentro de un FrameLayout
para superponer los elementos. Luego usar el valor @android:color/transparent
en el atributo android:background
de la Toolbar
.
Dirígete a la pestaña Design del layout fragmento_detalle_articulo.xml y luego selecciona el primer icono de la barra de herramientas del diseñador. Elige la opción Create Other…
Esto iniciará una ventana llamada Select Resource Directory, al cual te permite añadir variaciones de tus recursos dependiendo de los calificadores. En este caso usarás el calificador Screen Width con un valor de 900dp para indicar que el nuevo layout es solo para dispositivos que tengan como mínimo ese ancho.
Así se duplicará el código de fragmento_detalle_articulo.xml dentro de la carpeta res/layout-w900dp. En dicho archivo, debes añadir la nueva definición con Toolbar:
fragmento_detalle_articulo.xml (w900dp)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/articulo_detail" style="?android:attr/textAppearanceLarge" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".FragmentoDetalleArticulo"> <FrameLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="50" android:background="@color/backgroundActividad"> <ImageView android:id="@+id/imagen" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar_detalle" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@android:color/transparent" android:gravity="center_vertical" app:popupTheme="@style/AppTheme.PopupOverlay" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?colorPrimary" android:elevation="2dp" android:orientation="vertical" android:paddingBottom="@dimen/margen_estandar" android:paddingEnd="@dimen/margen_estandar" android:paddingLeft="@dimen/margen_izquierda_contenido" android:paddingRight="@dimen/margen_estandar" android:paddingStart="@dimen/margen_izquierda_contenido" android:paddingTop="@dimen/margen_estandar"> <TextView android:id="@+id/titulo" style="@style/TextAppearance.AppCompat.Title.Inverse" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="2" android:textIsSelectable="true" /> <TextView android:id="@+id/fecha" style="@style/TextAppearance.AppCompat.Subhead.Inverse" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="50" android:clipToPadding="false" android:paddingBottom="@dimen/margen_estandar" android:paddingTop="@dimen/margen_estandar" android:scrollbarStyle="outsideOverlay"> <TextView android:id="@+id/contenido" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingEnd="@dimen/margen_estandar" android:paddingLeft="@dimen/margen_izquierda_contenido" android:paddingRight="@dimen/margen_estandar" android:paddingStart="@dimen/margen_izquierda_contenido" android:text="Contenido" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> </ScrollView> </LinearLayout>
El código Java del fragmento debe permitir crear una instancia con un parámetro que sea el identificador del artículo. Recuerda que los argumentos de los fragmentos puedes obtenerlos con el método getArguments()
.
Con el id puedes cargar los datos del artículo en onCreateView()
y setearlos en cada view como se muestra a continuación:
FragmentoDetalleArticulo.java
import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.Toolbar; 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.tipsdesalud.modelo.ModeloArticulos; /** * Fragmento que representa el panel del detalle de un artículo. */ public class FragmentoDetalleArticulo extends Fragment { // EXTRA public static final String ID_ARTICULO = "extra.idArticulo"; // Artículo al cual está ligado la UI private ModeloArticulos.Articulo itemDetallado; public FragmentoDetalleArticulo() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments().containsKey(ID_ARTICULO)) { // Cargar modelo según el identificador itemDetallado = ModeloArticulos.MAPA_ITEMS.get(getArguments().getString(ID_ARTICULO)); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragmento_detalle_articulo, container, false); if (itemDetallado != null) { // Toolbar en master-detail Toolbar toolbar = (Toolbar) v.findViewById(R.id.toolbar_detalle); if (toolbar != null) toolbar.inflateMenu(R.menu.menu_detalle_articulo); ((TextView) v.findViewById(R.id.titulo)).setText(itemDetallado.titulo); ((TextView) v.findViewById(R.id.fecha)).setText(itemDetallado.fecha); ((TextView) v.findViewById(R.id.contenido)).setText(getText(R.string.lorem)); Glide.with(this) .load(itemDetallado.urlMiniatura) .into((ImageView) v.findViewById(R.id.imagen)); } return v; } }
Paso 3. Infla el fragmento en el contenedor de la actividad de detalle a través de una transacción. Recuerda habilitar el up button si se trata de teléfonos.
ActividadDetalleArticulo.java
import android.content.Intent; import android.os.Bundle; import android.support.v4.app.NavUtils; 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; /** * Actividad que muestra el detalle del artículo seleccionado en {@link ActividadListaArticulos} */ public class ActividadDetalleArticulo extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.actividad_detalle_articulo); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_detalle); setSupportActionBar(toolbar); // Verificación: ¿La app se está ejecutando en un teléfono? if (!getResources().getBoolean(R.bool.esTablet)) { // Mostrar Up Button ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } if (savedInstanceState == null) { // Añadir fragmento de detalle Bundle arguments = new Bundle(); arguments.putString(FragmentoDetalleArticulo.ID_ARTICULO, getIntent().getStringExtra(FragmentoDetalleArticulo.ID_ARTICULO)); FragmentoDetalleArticulo fragment = new FragmentoDetalleArticulo(); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() .add(R.id.contenedor_detalle_articulo, fragment) .commit(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_detalle_articulo, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { NavUtils.navigateUpTo(this, new Intent(this, ActividadListaArticulos.class)); return true; } return super.onOptionsItemSelected(item); } }
Para determinar si estás en una teléfono o una tablet, puedes usar un recurso bool que sea false
en la carpeta values general y que sea true
en values-w900dp.
<bool name="esTablet">false</bool>
Luego puedes obtener la referencia a través de getResources().getBoolean()
y condicionar un comportamiento (en este caso, el mostrar el up button si es teléfono).
7. Manejar Eventos De Master-Detail
Paso 1. Para que generes la interacción donde se selecciona un ítem de la lista e inmediatamente se carga su detalle en el panel (o se inicia la actividad detalle si es un teléfono) es necesario añadir las acciones en el controlador de la interfaz del fragmento de la lista.
public void cargarDetalle(String idArticulo) { if (escucha != null) { escucha.alSeleccionarItem(idArticulo); } } @Override public void onClick(AdaptadorArticulos.ViewHolder viewHolder, String idArticulo) { cargarDetalle(idArticulo); }
Solo es necesario que se ejecute el controlador EscuchaFragmento.alSeleccionarItem()
dentro del controlador OnItemClickListener.onClick()
. Esto propaga el click en cada ítem de la lista hacia la actividad.
Paso 2. Dentro de onCreate()
en ActividadListaArticulos
determina si la app corre en teléfono o tablet. Guarda dicho resultado en una variable global y luego úsala a tu favor para condicionar el flujo de ejecución.
ActividadListaArticulos.java
import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos; /** * Actividad con la lista de artículos. Si el ancho del dispositivo es mayor o igual a 900dp, entonces * se incrusta el fragmento de detalle {@link FragmentoDetalleArticulo} para generar el patrón * Master-detail */ public class ActividadListaArticulos extends AppCompatActivity implements FragmentoListaArticulos.EscuchaFragmento { // ¿Hay dos paneles? private boolean dosPaneles; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.actividad_lista_articulos); ((Toolbar) findViewById(R.id.toolbar)).setTitle(getTitle()); // Verificación: ¿Existe el detalle en el layout? if (findViewById(R.id.contenedor_detalle_articulo) != null) { // Si es asi, entonces confirmar modo Master-Detail dosPaneles = true; cargarFragmentoDetalle(ModeloArticulos.ITEMS.get(0).id); } // Agregar fragmento de lista getSupportFragmentManager() .beginTransaction() .replace(R.id.contenedor_lista, FragmentoListaArticulos.crear()) .commit(); } private void cargarFragmentoDetalle(String id) { Bundle arguments = new Bundle(); arguments.putString(FragmentoDetalleArticulo.ID_ARTICULO, id); FragmentoDetalleArticulo fragment = new FragmentoDetalleArticulo(); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() .replace(R.id.contenedor_detalle_articulo, fragment) .commit(); } @Override public void alSeleccionarItem(String idArticulo) { if (dosPaneles) { cargarFragmentoDetalle(idArticulo); } else { Intent intent = new Intent(this, ActividadDetalleArticulo.class); intent.putExtra(FragmentoDetalleArticulo.ID_ARTICULO, idArticulo); startActivity(intent); } } }
La variable dosPaneles
tiene el valor de true si contenedor_detalle_articulo
existe, de lo contrario es false
. Aunque también puedes usar el recurso bool esTablet
.
Si hay dos paneles, entonces se inserta un nuevo fragmento de detalle con el identificador que envía el adaptador. De lo contrario se inicia ActividadDetalleArticulo
.
El primer ítem es seleccionado cuando se determina que es una tableta a través del método cargarFragmentoDetalle()
, donde el parámetro el id del elemento que se encuentra en la posición 0 dentro de ModeloArticulos.ITEMS
.
Punto de ejecución — Al relacionar los ítems de la lista por su identificador con el detalle del fragmento ya te es posible ejecutar la app en un emulador de tableta (yo usé el Nexus 10) y contemplar el siguiente resultado:
8. Android Studio: Template Para Master-Detail
Android Studio también posee un template que te permite generar el master-detail con tan solo un click.
Una de las formas de hacerlo es al momento de crear nuevo proyecto, donde puedes elegir como actividad principal un patrón master-detail.
Dentro de este te permitirán elegir el nombre de los elementos que listarás y la forma de referirte a ellos en plural.
Otra forma es ir a New > Activity > Master/Detail Flow. Esta vez puedes configurar un padre de la jerarquía para la actividad.
Saber que existe esta plantilla te ahorrará gran cantidad de tiempo la próxima vez que inicies un proyecto que tenga un patrón master-detail. Aunque el contenido del template es básico, es una buena base para realizar modificaciones.
Conclusión
Este artículo te mostró algunas ideas para aprovechar el espacio en blanco en tablets para crear un patrón master-detail.
Claramente se ve la diferencia entre dejar solo los ítems en una extensa pantalla y combinar dos fragmentos para construir una nueva interfaz.
Hay innumerables formas de crear el master-detail dependiendo de cada app en específico. Algunos no usarán una toolbar por cada fragmento como hicimos y algotros incluso tendrán más elementos de interacción. Todo depende de tus necesidades.
Recuerda que Android Studio tiene varios templates para optimizar tus desarrollos, así que haz uso de estos elementos para generar elementos básicos que puedan ser extendidos. Esto te ahorrará gran cantidad de tiempo.