Aprenderemos a programar un menú lateral que abre un CardView en Android. Una vez que tenemos elaborado un menú lateral, de seguro queremos que al darle clic a alguna de sus opciones, nos realice alguna acción. En éste artículo veremos cómo mostrar un CardView utilizando el componente RecyclerView.
Éste artículo está explicado en video: https://youtu.be/PINqvwe8z54
Un CardView nos permitirá presentar información de una forma coherente y organizada (Griffits, 2015).
En el ejemplo anterior, se tiene un artículo que forma parte de una lista de artículos, cada uno tiene un título (“¿Qué es el sobrepeso?”), un subtítulo (“Y en qué es diferente de la obesidad”), una imagen, la leyenda Saber más junto con un icono de flecha hacia abajo, y un botón que abre un menú contextual.
RecyclerView es una de las vistas más comúnmente usadas en Android. Viene siendo una versión más avanzada de ListView. Es un contenedor que permite administrar un conjunto grande de datos o información, al reciclarlos eficientemente a medida que la lista es desplazada hacia arriba o hacia abajo (Guleria, 2019).
En los siguientes pasos, considero que ya tienes desarrollado un Menú Lateral. En caso de que no fuese así, te invito a realizarlo siguiendo los pasos que se muestran en el siguiente enlace: Pasos para desarrollar un menú en una app Android.
Veamos ahora los pasos para programar el menú lateral que abre un CardView
Abre tu archivo build.gradle (Module:app) y en la sección dependencies añade las siguientes:
implementation 'com.android.support:cardview-v7:27.1.1' implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.squareup.picasso:picasso:2.5.2'
El modelo de datos es una nueva clase Java. Ubica el paquete donde crearás ésta clase. Puede ser tu com o un package dentro de tu com. Por ejemplo:
En la imagen de arriba, la clase que creamos se llama Dato y está dentro de un package llamdo Models. (Pero puede estar en el com.naps.aaa.bbb). Los packages Activities, Adapter, Clases y Fragments los he creado para organizar mejor la aplicación.
Da clic derecho y selecciona New->Java Class.
El código de la clase Dato.java es el siguiente:
public class Dato { private String titulo; private String subtitulo; private int imagen; private String contenido; // Constructor de la clase public Dato(String titulo, String subtitulo, int imagen, String contenido) { this.titulo = titulo; this.subtitulo = subtitulo; this.imagen = imagen; this.contenido = contenido; } // Metodos setter y getter public String getSubtitulo() { return subtitulo; } public void setSubtitulo(String subtitulo) { this.subtitulo = subtitulo; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } public void setContenido(String contenido){ this.contenido = contenido; }; public int getImagen() { return imagen; } public String getContenido() { return contenido; } public void setImagen(int imagen) { this.imagen = imagen; } }
Clic derecho sobre el package donde vas a crear el fragment luego en New->Fragment->Fragment (Blank)
Abre el archivo fragment_informacion.xml que se encuentra dentro de res->layout
En éste archivo agregaremos un componente RecyclerView. El archivo quedará más o menos de la siguiente forma (considera que la linea que indica el context cambiará en tu proyecto):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.naps.gibgarcia.npsalud.Fragments.InformacionFragment"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"/> </FrameLayout>
Deberás agregar las líneas del componente RecyclerView únicamente.
Dentro del RecyclerView irán los item de información con el diseño que mostramos arriba, en la Fig. 1.
Vamos a diseñar ese layout.
En res->layout, da clic derecho, luego en New->Layout resource file
Como nombre de archivo, usaré list_item_informacion.xml
A continuación te muestro cómo quedó el código que utilicé.
Se crea un componente CardView. Dentro, va un LinearLayout vertical. Éste LinearLayout tiene tres elementos: un Toolbar (que contiene el título y el subtítulo), un ImageView (para la imagen), otro LinearLayout (ésta vez horizontal), que contiene un TextView (que lleva la frase “Saber más”), y un ImageView (con una flecha hacia abajo). Notarás otro LinearLayout, y es el que contiene la información que aparecerá cuando se le de clic a la flecha hacia abajo para “saber más”.
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/cardView" android:layout_margin="16dp" app:cardUseCompatPadding="true" app:cardCornerRadius="4dp" tools:context="com.naps.gibgarcia.npsalud.Fragments.InformacionFragment"> <!-- app:cardBackgroundColor="#E2E2E2" --> <LinearLayout android:id="@+id/linearLayoutCardContent" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/toolbarCard" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" app:popupTheme="@style/Theme.AppCompat.Light" app:subtitleTextAppearance="@style/Card.Subtitle" app:theme="@style/ToolbarCard" app:titleTextAppearance="@style/Card.Title" /> <ImageView android:id="@+id/imageViewPoster" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:adjustViewBounds="true" android:contentDescription="contentDescription" android:src="@drawable/blur" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/unlinearl" android:orientation="horizontal" android:padding="8dp"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" android:gravity="center_vertical" android:text="Saber más" android:textColor="@android:color/black" android:textSize="20sp" /> <ImageView android:id="@+id/imageViewExpand" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" android:contentDescription="Details" android:src="@drawable/more" /> </LinearLayout> <LinearLayout android:id="@+id/linearLayoutDetails" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingLeft="8dp" android:visibility="gone"> <TextView android:id="@+id/textViewInfo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Infoo" android:textColor="@android:color/black" android:textSize="16sp" /> </LinearLayout> </LinearLayout> </android.support.v7.widget.CardView>
Notas que utiliza algunos estilos que aún no están establecidos. Vamos a crearlos en el archivo res->values->styles.xml
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <style name="ToolbarCard" parent="ThemeOverlay.AppCompat.ActionBar"> <item name="colorPrimary">@android:color/black</item> <item name="android:textColorPrimary">@android:color/black</item> </style> <style name="Card.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title"> <item name="android:textSize">24sp</item> </style> <style name="Card.Subtitle" parent="TextAppearance.Widget.AppCompat.Toolbar.Subtitle"> <item name="android:textSize">16sp</item> <item name="android:textColor">@android:color/darker_gray</item> </style> </resources>
Tú solo agrega las líneas correspondientes a los 3 estilos ToolbarCard, Title y Subtitle.
Se necesita un adapter que realice un enlace entre los datos que deseamos mostrar, y la vista que los desplegará en pantalla, en éste caso un RecyclerView.
Damos clic derecho sobre el package donde lo crearemos, luego en New->Java Class, y como nombre, usaré InformacionAdapter.
// Hereda de RecyclerView.Adapter public class InformacionAdapter extends RecyclerView.Adapter<InformacionAdapter.ViewHolder> { // Una lista de objetos "Dato" private List<Dato> datos; // id del layout list_item_informacion private int layout; private OnItemClickListener itemClickListener; private Context context; private static final int DURATION = 250; public InformacionAdapter(List<Dato> datos, int layout, OnItemClickListener listener) { this.datos = datos; this.layout = layout; this.itemClickListener = listener; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(layout, parent, false); context = parent.getContext(); ViewHolder vh = new ViewHolder(v); return vh; } @Override public void onBindViewHolder( ViewHolder holder, int position) { holder.bind(datos.get(position), itemClickListener); } @Override public int getItemCount() { return datos.size(); } public class ViewHolder extends RecyclerView.ViewHolder{ // Elementos de diseño de list_item_informacion.xml public ImageView imageViewPoster; public ViewGroup linearLayoutDetails; public Toolbar toolbarCard; public LinearLayout unlinearl; public ImageView imageViewExpand; public TextView textViewInfo; public ViewHolder(View itemView){ super(itemView); // Enlazamos los elementos de la vista imageViewPoster = itemView.findViewById(R.id.imageViewPoster); linearLayoutDetails = itemView.findViewById(R.id.linearLayoutDetails); toolbarCard = itemView.findViewById(R.id.toolbarCard); unlinearl = itemView.findViewById(R.id.unlinearl); imageViewExpand = itemView.findViewById(R.id.imageViewExpand); textViewInfo = itemView.findViewById(R.id.textViewInfo); } public void bind(final Dato dato, final OnItemClickListener listener){ // Sustituimos los elementos por su valor dentro de la lista de datos toolbarCard.setTitle(dato.getTitulo()); toolbarCard.setSubtitle(dato.getSubtitulo()); // Cree un menu dentro de res->layout->menu con el nombre card_menu toolbarCard.inflateMenu(R.menu.card_menu); toolbarCard.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_option1: Toast.makeText(context, "op1", Toast.LENGTH_SHORT).show(); break; case R.id.action_option2: Toast.makeText(context, "op2", Toast.LENGTH_SHORT).show(); break; case R.id.action_option3: Toast.makeText(context, "op3", Toast.LENGTH_SHORT).show(); break; } return true; } }); textViewInfo.setText(dato.getContenido()); // Mostrar y ocultar la información unlinearl.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (linearLayoutDetails.getVisibility() == View.GONE) { // Cree la clase ExpandAndCollpaseViewUtil ExpandAndCollapseViewUtil.expand(linearLayoutDetails, DURATION); imageViewExpand.setImageResource(R.drawable.more); // Rotamos el icono de flecha 180 grados Animation animation = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setFillAfter(true); animation.setDuration(DURATION); imageViewExpand.startAnimation(animation); } else { ExpandAndCollapseViewUtil.collapse(linearLayoutDetails, DURATION); imageViewExpand.setImageResource(R.drawable.less); Animation animation = new RotateAnimation(0.0f, 180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setFillAfter(true); animation.setDuration(DURATION); imageViewExpand.startAnimation(animation); } } }); // Picasso mejora el uso de imagenes // square.github.io/picasso Picasso.with(context).load(dato.getImagen()) .into(imageViewPoster); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { listener.onItemClick(dato, getAdapterPosition()); } }); } } public interface OnItemClickListener{ void onItemClick(Dato dato, int position); } }
Deseamos que cada tarjeta contenga un menú similar a éste:
En el código de adapter hay una línea que manda a llamar ese menú:
toolbarCard.inflateMenu(R.menu.card_menu);
Pero hay que crear ese menú. Ya hay un directorio de menú que creamos en el artículo anterior. Está en res->layout->menu
De clic derecho sobre el directorio menu, luego en New->Menu Resource File
Cree un nuevo archivo con el nombre: card_menu.xml
El código de éste archivo será similar a éste:
<?xml version="1.0" encoding="utf-8"?> <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=".MainActivity"> <item android:id="@+id/action_option1" android:title="Compartir" app:showAsAction="never" /> <item android:id="@+id/action_option2" android:title="Enviar a mi correo" app:showAsAction="never" /> <item android:id="@+id/action_option3" android:title="Archivar" app:showAsAction="never" /> </menu>
Observas que contiene las tres opciones de la ilustración, con sus id correspondientes.
Al dar clic al icono a la derecha de “saber más”, se expande la tarjeta, mostrando información. El icono rota y se muestra como una flecha hacia arriba, y al dar clic sobre ésta, la tarjeta se colapsa, ocultando la información.
Para realizar éste efecto crearemos una clase llamada ExpandAndCollapseViewUtil.
De clic derecho sobre el package donde va a crear la clase. Luego en New->Java Class.
El código de ésta clase es el siguiente:
public class ExpandAndCollapseViewUtil { public static void expand(final ViewGroup v, int duration) { slide(v, duration, true); } public static void collapse(final ViewGroup v, int duration) { slide(v, duration, false); } private static void slide(final ViewGroup v, int duration, final boolean expand) { try { //onmeasure method is protected Method m = v.getClass().getDeclaredMethod("onMeasure", int.class, int.class); m.setAccessible(true); m.invoke( v, View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getMeasuredWidth(), View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ); } catch (Exception e) { Log.e("slideAnimation", e.getMessage(), e); } final int initialHeight = v.getMeasuredHeight(); if (expand) { v.getLayoutParams().height = 0; } else { v.getLayoutParams().height = initialHeight; } v.setVisibility(View.VISIBLE); Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { int newHeight = 0; if (expand) { newHeight = (int) (initialHeight * interpolatedTime); } else { newHeight = (int) (initialHeight * (1 - interpolatedTime)); } v.getLayoutParams().height = newHeight; v.requestLayout(); if (interpolatedTime == 1 && !expand) { v.setVisibility(View.GONE); } } @Override public boolean willChangeBounds() { return true; } }; a.setDuration(duration); v.startAnimation(a); } }
Vamos al archivo InformacionFragment, que es el fragment que contendrá nuestros CardView. Recuerda que el layout de éste fragment, ya contiene un RecyclerView.
En el método onCreate, enlazamos todo.
En el método getAllData (es un método propio), se crea un ArrayList de objetos de clase “Dato”, con algunos valores de ejemplo. Ésta lista es pasada al adapter que creamos, junto con el layout de los items.
El código será como el siguiente:
public class InformacionFragment extends Fragment { private List<Dato> datos; private RecyclerView.Adapter mAdapter; private RecyclerView.LayoutManager mLayoutManager; private RecyclerView mRecyclerView; public InformacionFragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_informacion, container, false); datos = this.getAllData(); mRecyclerView = view.findViewById(R.id.recyclerView); mLayoutManager = new LinearLayoutManager(getContext()); mAdapter = new InformacionAdapter(datos, R.layout.list_item_informacion, new InformacionAdapter.OnItemClickListener() { @Override public void onItemClick(Dato dato, int position) { } }); mRecyclerView.setHasFixedSize(true); // Añade un efecto por defecto, si le pasamos null lo desactivamos por completo mRecyclerView.setItemAnimator(new DefaultItemAnimator()); // Enlazamos el layout manager y adaptador directamente al recycler view mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setAdapter(mAdapter); return view; } private List<Dato> getAllData(){ return new ArrayList<Dato>() {{ add(new Dato("¿Qué es el sobrepeso?","Y en qué es diferente de la obesidad", R.drawable.obesity, "La obesidad se define como un aumento de composición de grasa corporal. Este aumento se traduce en un incremento del peso y aunque no todo incremento del peso corporal es debido a un aumento del tejido adiposo, en la práctica médica el concepto de obesidad está relacionado con el peso corporal. La obesidad debe ser entendida como una enfermedad crónica, de forma semejante que lo es la diabetes mellitus o la hipertensión arterial. (Fuente: Infosalus.com)\n" + "\n ")); add(new Dato("¿Qué es el IMC?", "Por qué es importante saberlo",R.drawable.imc, "")); add(new Dato("¿Cómo medir la cintura?", "Cómo saber si tienes la medida correcta",R.drawable.belly, "")); add(new Dato("Alimentos que acumulan grasa abdominal", "Y que debes evitar",R.drawable.food, "")); }}; } }
Vamos al MainActivity. (Recuerda que en el artículo Pasos para desarrollar un menú en una app Android creamos un menú con una opción que se llama Información. Ésta es la opción que usaremos para abrir nuestro fragment que contiene el RecyclerView con CardView).
Busca la línea que enlaza a navigationView. Se ve así:
navigationView = (NavigationView) findViewById(R.id.navview);
Debajo de esa línea escribiremos el código que detectará cuando se da un clic a una opción del menú. Es éste:
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { boolean fragmentTransaction = false; switch (item.getItemId()){ case R.id.menu_informacion: frg = new InformacionFragment(); changeFragment(frg, item); drawerLayout.closeDrawers(); fragmentTransaction = false; break; } if (fragmentTransaction){ changeFragment (fragment , item ); drawerLayout.closeDrawers(); } return true; }
Si ya habías creado éste Listener, no debes crear otro, sino únicamente añadir el caso correspondiente:
case R.id.menu_informacion:
frg = new InformacionFragment();
changeFragment(frg, item);
drawerLayout.closeDrawers();
fragmentTransaction = false;
break;
Notas que utiliza una función propia llamada changeFragment. Ésta función va una vez cierres la llave del onCreate, y contiene el siguiente código:
private void changeFragment( Fragment fragment, MenuItem item){ getSupportFragmentManager() .beginTransaction() .replace(R.id.content_frame, fragment) .commit(); item.setChecked(true); getSupportActionBar().setTitle(item.getTitle()); }
¡Listo! Al darle clic a la opción Información del Menú Lateral, se abre un fragment que contiene un RecyclerView, con una lista de CardView. Espero logres completar tu proyecto en programar un menú lateral que abre un CardView en Android.
Mira el resultado y los pasos ya descritos en video: