Diseño Android: Tarjetas con Material Components y contenido expandible

Última actualización: 12/11/2020

android

Las tarjetas son un componente gráfico popularizado 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 las antiguas librerías 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.0 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_marginTop="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 base, hay que utilizar temas de MaterialComponents.

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

   <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
        <item name="android:textColorPrimary">@color/textColorPrimary</item>
    </style>

    <style name="AppTheme.ToolbarMain" parent="Widget.MaterialComponents.Toolbar.Primary">
        <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>
        <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 aunque obsérvese los bordes redondeados y la sombra (elevación) pertenecientes a la CardView, elementos configurados en los estilos.

android cardview demo

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="cardBackgroundColor">@android:color/white</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>

android cardview custom stroke

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_marginTop="8dp"
                    android:adjustViewBounds="true"
                    android:contentDescription="@string/olinguito"
                    android:src="@drawable/olinguito" />

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

    </ScrollView>


</LinearLayout>

Cursos aplicaciones móviles

Para personalizar la Toolbar nuevamente se ha recurrido a la creación de un estilo.

   <style name="ToolbarCard" parent="Widget.MaterialComponents.Toolbar">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="titleTextColor">@color/textColorTitle</item>
        <item name="popupTheme">@style/Theme.MaterialComponents.Light</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 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;
      }
    });
  }

}

android cardview and toolbar

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.

android collapsible layout

 <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_marginTop="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:textColor="@color/textColorTitle"
                        android:textSize="20sp" />

                    <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: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:visibility="gone">

                    <TextView
                        android:id="@+id/textViewInfo"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/info"
                        android:textColor="@color/textColorTitle"
                        android:textSize="16sp" />
                </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 del proyecto de ejemplo.


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.

Master Pyhton, Java, Scala or Ruby

4 comentarios sobre “Diseño Android: Tarjetas con Material Components y contenido expandible

  1. 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.

  2. 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

  3. 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!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .