Las tarjetas son un componente gráfico popularizado en Android por Google Now hace ya bastantes años y que forman parte de las guías de estilos y componentes gráficos Material Design creadas por Google. El concepto de tarjeta es el de una ficha de papel o portada de una carpeta que proporciona sólo el resumen de cierto contenido. El resumen puede estar compuesto por un conjunto de elementos heterogéneos que incluye imágenes, títulos, textos, íconos, menús contextuales, acciones…
Contamos con una implementación oficial para Android del componente tarjeta en el widget CardView empaquetado en un módulo propio de AndroidX (la evolución de la ya obsoleta biblioteca de compatibilidad). Asimismo, en la novedosa librería Material Components se incluye una especialización de este widget llamada MaterialCardView que añade algunas mejoras para soportar las últimas versiones de Material Design y que a día es el widget más recomendable para utilizar tarjetas en nuestras aplicaciones.
En este tutorial implementaremos una tarjeta más o menos típica utilizando Material Components, tanto la propia card como la toolbar (MaterialToolbar). Lo haremos en un proyecto de ejemplo compatible con Android Studio 4.1 y que se compilará para Android Api 30, siendo compatible hasta Android 4.4 KitKat.
Nota Las primeras versiones de este pequeño artículo utilizaban el componente CardView. Se puede consultar el proyecto de ejemplo correspondiente en esta rama.
En primer lugar, debemos incluir en nuestro proyecto las librerías que proporcionan los widgets de Material Components. Sólo hay que añadir una dependencia.
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.google.android.material:material:1.2.1' }
MaterialCardView es una subclase de FrameLayout por lo que podemos considerar este widget como un Layout más cuyos hijos se pueden superponer. Vamos a crear una tarjeta que simplemente muestre una imagen del olinguito. El layout contiene además una Toolbar a modo de ActionBar y la tarjeta ha sido incluida en un ScrollView.
<LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" style="@style/AppTheme.ToolbarMain" /> <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:scrollbars="none"> <com.google.android.material.card.MaterialCardView android:id="@+id/card" style="@style/AppTheme.CardView"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:adjustViewBounds="true" android:contentDescription="@string/olinguito" android:src="@drawable/olinguito" /> </com.google.android.material.card.MaterialCardView> </ScrollView> </LinearLayout>
Para facilitar la reutilización de los componentes, tanto la toolbar como la tarjeta han sido configuradas mediante estilos. Como tema base de la aplicación, tenemos el DayNight de Material Components que proporciona automáticamente soporte para tema claro y oscuro.
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">@color/primary</item> <item name="colorPrimaryDark">@color/primaryDark</item> <item name="colorOnPrimary">@android:color/white</item> </style> <!--colorPrimary en modo claro, colorSurface en modo oscuro --> <style name="AppTheme.ToolbarMain" parent="Widget.MaterialComponents.Toolbar.PrimarySurface"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">?attr/actionBarSize</item> <item name="android:elevation" tools:targetApi="lollipop">4dp</item> </style> <style name="AppTheme.CardView"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <!-- por defecto se aplica el colorSurface del tema--> <!--<item name="cardBackgroundColor">@android:color/white</item>--> <item name="cardCornerRadius">4dp</item> <item name="cardElevation">4dp</item> <item name="cardUseCompatPadding">true</item> </style> </resources>
El resultado que tenemos es simplemente el ImageView dentro de la tarjeta aunque obsérvese los bordes redondeados y la sombra (elevación) pertenecientes a la CardView, elementos configurados en los estilos. El color de fondo de la card se corresponde con el colorSurface del tema, que no hemos personalizada y que por defecto proporciona un color adecuado tanto para modo claro. Podemos cambiarlo explícitamente sólo para la card con la propiedad cardBackgroundColor.
Nota Se puede cambiar fácilmente entre tema claro y oscuro activando en Android 10+ el modo ahorro de batería o bien el tema oscuro en las preferencias. Recomiendo consultar Android: Selección de tema claro y oscuro, pantalla de ajustes.
La línea del borde es personalizable gracias a las propiedades strokeWidth y strokeColor.
<style name="AppTheme.CardView"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <item name="cardCornerRadius">4dp</item> <item name="cardElevation">4dp</item> <item name="cardUseCompatPadding">true</item> <item name="strokeWidth">3dp</item> <item name="strokeColor">@android:color/black</item> </style>
Toolbar
Vamos a añadir una cabecera a la tarjeta que muestre un título, un subtítulo y un menú desplegable con varias acciones utilizando para ello una Toolbar. La tarjeta ahora contendrá un LinearLayout con la Toolbar y el ImageView.
<LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" style="@style/AppTheme.ToolbarMain" /> <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:scrollbars="none"> <com.google.android.material.card.MaterialCardView android:id="@+id/card" style="@style/AppTheme.CardView"> <LinearLayout android:id="@+id/linearLayoutCardContent" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbarCard" style="@style/ToolbarCard"/> <!--By Mark Gurney [CC BY 3.0 (http://creativecommons.org/licenses/by/3.0)], via Wikimedia Commons --> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:adjustViewBounds="true" android:contentDescription="@string/olinguito" android:src="@drawable/olinguito" /> </LinearLayout> </com.google.android.material.card.MaterialCardView> </ScrollView> </LinearLayout>
Para personalizar la Toolbar nuevamente se ha recurrido a la creación de un estilo, y queremos que su color de fondo sea el mismo que el de la card.
<style name="ToolbarCard" parent="Widget.MaterialComponents.Toolbar"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <!-- estilo del menú adecuado para modo claro y modo oscuro --> <item name="popupTheme">@style/Theme.MaterialComponents.DayNight</item> </style>
El menú se define como es habitual en el xml correspondiente. En nuestro caso queremos que las opciones se muestren siempre en un menú contextual (showAsAction tendrá el valor never), pero también se podrían mostrar la acciones del mismo modo que en cualquier Toolbar, por ejemplo con iconos.
<?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="@string/option1" app:showAsAction="never" /> <item android:id="@+id/action_option2" android:title="@string/option2" app:showAsAction="never" /> <item android:id="@+id/action_option3" android:title="@string/option3" app:showAsAction="never" /> </menu>
El título, subtítulo y menú de la Toolbar de la tarjeta también se pueden definir directamente en el layout.
<com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbarCard" style="@style/ToolbarCard" app:title="@string/olinguito" app:subtitle="@string/subtitle" app:menu="@menu/menu_card"/>
Pero normalmente la card será dinámica y estos valores tendrán que ser configurados mediante código. También tendremos que asignar la Toolbar un listener de tipo Toolbar.OnMenuItemClickListener para atender las pulsaciones de las acciones. Simplemente se mostrará un Toast con la opción seleccionada.
package com.danielme.android.cardview; import android.os.Bundle; import android.view.MenuItem; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setupToolbar(); setupCard(); } private void setupToolbar() { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); } private void setupCard() { Toolbar toolbarCard = findViewById(R.id.toolbarCard); toolbarCard.setTitle(R.string.olinguito); toolbarCard.setSubtitle(R.string.subtitle); toolbarCard.inflateMenu(R.menu.menu_card); toolbarCard.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_option1: Toast.makeText(MainActivity.this, R.string.option1, Toast.LENGTH_SHORT).show(); break; case R.id.action_option2: Toast.makeText(MainActivity.this, R.string.option2, Toast.LENGTH_SHORT).show(); break; case R.id.action_option3: Toast.makeText(MainActivity.this, R.string.option3, Toast.LENGTH_SHORT).show(); break; } return true; } }); } }
Secciones expandibles
Veamos cómo añadir una sección expandible tipo acordeón que haga uso de animaciones para mostrar y ocultar el contenido.
Añadimos dos nuevos layout a la tarjeta: la cabecera de la sección expandible (título y botón) y la información que se mostrará al expandirse la sección.
<LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" style="@style/AppTheme.ToolbarMain" /> <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:scrollbars="none"> <com.google.android.material.card.MaterialCardView android:id="@+id/card" style="@style/AppTheme.CardView"> <LinearLayout android:id="@+id/linearLayoutCardContent" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbarCard" style="@style/ToolbarCard" /> <!--app:title="@string/olinguito" app:subtitle="@string/subtitle" app:menu="@menu/menu_card" --> <!--By Mark Gurney [CC BY 3.0 (http://creativecommons.org/licenses/by/3.0)], via Wikimedia Commons --> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:adjustViewBounds="true" android:contentDescription="@string/olinguito" android:src="@drawable/olinguito" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" 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="@string/details" android:textAppearance="?attr/textAppearanceHeadline6" /> <ImageButton android:id="@+id/imageViewExpand" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/details" android:background="?attr/selectableItemBackground" android:src="@mipmap/more" android:tint="?attr/colorOnSurface" android:onClick="toggleDetails"/> </LinearLayout> <LinearLayout android:id="@+id/linearLayoutDetails" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingBottom="16dp" android:visibility="gone"> <TextView android:id="@+id/textViewInfo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/info" android:textAppearance="?attr/textAppearanceSubtitle1" /> </LinearLayout> </LinearLayout> </com.google.android.material.card.MaterialCardView> </ScrollView> </LinearLayout>
La pulsación del botón está asociada al método toggleDetails cuyo cometido es mostrar u ocultar el linearLayoutDetails según su estado actual. Esta acción se realizará cambiando la visibilidad del layout de GONE a VISIBLE y viceversa, pero para que el resultado final sea refinado y elegante vamos a utilizar animaciones:
- Si el linearLayoutDetails está oculto, lo mostraremos deslizándolo hacia abajo empezando en la cabecera. También se cambiará el icono con la flecha rotándolo sobre sí mismo 180º
- En caso contrario, las animaciones se realizarán de forma inversa de tal modo que el linearLayoutDetails se deslice hacia arriba hasta desaparecer debajo de la tarjeta y el icono con la flecha «vuelva» a su posición anterior
La animación del icono flecha es sencilla y para el deslizado del layout se podría simplemente aplicar la api transition, disponible en androidx.transition, del siguiente modo
Transition transition = new AutoTransition(); transition.setDuration(DURATION); TransitionManager.beginDelayedTransition(cardView, transition); linearLayoutDetails.setVisibility(linearLayoutDetails.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
Sin embargo el efecto resultante no me convence. Implementar esta animación manualmente resulta más complejo de lo que puede aparecer a simple vista ya que es necesario calcular la altura que debe ocupar el layout a medida que se va deslizando y además hay que tener en cuenta que su contenido tiene una altura variable (en el ejemplo se utiliza un TextView que puede tener múltiples líneas). Para esta animación de deslizado voy a utilizar este código que he refactorizado con algunos pequeños cambios en la clase ExpandAndCollapseViewUtil.
El nuevo código necesario en la MainActivity es el siguiente (no olvidar obtener las vistas en el onCreate)
private ViewGroup linearLayoutDetails; private ImageView imageViewExpand; private static final int DURATION = 250; public void toggleDetails(View view) { if (linearLayoutDetails.getVisibility() == View.GONE) { ExpandAndCollapseViewUtil.expand(linearLayoutDetails, DURATION); imageViewExpand.setImageResource(R.mipmap.more); rotate(-180.0f); } else { ExpandAndCollapseViewUtil.collapse(linearLayoutDetails, DURATION); imageViewExpand.setImageResource(R.mipmap.less); rotate(180.0f); } } private void rotate(float angle) { Animation animation = new RotateAnimation(0.0f, angle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setFillAfter(true); animation.setDuration(DURATION); imageViewExpand.startAnimation(animation); }
El siguiente video muestra el resultado final.
Código de ejemplo
El proyecto de ejemplo para Android Studio se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Buenas, a la hora de incluir las dependencias me da error, y no se como solucionarlo, si me pudieras ayudar lo agradecería mucho.
Muchas gracias, un saludo.
hola excelente tutorial.
pero tengo un error al crear la ultima linea de código la aplicación se destruye o podria explicar como se obtiene la vistas en el onCreate ,
muchas gracias
Hola! una consulta, en «linearLayoutDetails» al expandir la vista, si el contenido es largo y sobrepasa el alto de la pantalla, ¿cómo hacés para ir al final del linearLayoutDetails? gracias, saludos!
Thank you very much! This article helped me a lot!!