Diseño de tarjetas

En este documento, se explica cómo seguir el estilo de Glass y cómo implementar prácticas recomendadas comunes de la IU cuando se usa la GDK.

Tema de Glass

Glass aplica un tema estándar a tu Glassware, por lo que se mantiene coherente con el resto de la interfaz de usuario. El tema tiene las siguientes características:

  • Usa el tipo de letra Roboto
  • Muestra las actividades en pantalla completa sin barra de estado ni de acción
  • Aplica un fondo negro sólido.

Para aplicar el tema de Glass, no declares un tema en tu manifiesto de Android.

Si tienes un estilo personalizado para partes de tu Glassware y quieres usar el tema predeterminado de Glass para todo lo demás, hereda de Theme.DeviceDefault con el atributo parent:

<resources>
    <style name="CustomTheme" parent="@android:style/Theme.DeviceDefault">
        <!-- Theme customization goes here. -->
    </style>
</resources>

Consulta la guía para desarrolladores de Android sobre estilos y temas a fin de obtener más información sobre cómo crear temas.

Tarjetas con estilo de vidrio

La clase CardBuilder crea tarjetas con el formato correcto según un conjunto de propiedades. Usa los diseños proporcionados por CardBuilder.Layout siempre que sea posible para que tu contenido se vea y se sienta como otro contenido en Glass.

Para usar CardBuilder, haz lo siguiente:

  1. Crea una instancia de CardBuilder y asígnale el diseño que desees desde CardBuilder.Layout.
  2. Configura las propiedades de la tarjeta, como el texto, la nota al pie y la marca de tiempo.
  3. Llama a CardBuilder.getView() para convertir la tarjeta en un View de Android, o a CardBuilder.getRemoteViews() para convertirla en un objeto RemoteViews.
  4. Usa View en tus actividades, diseños o CardScrollView, o usa RemoteViews en LiveCard.

Funciones comunes de la IU

Muchos de los diseños que proporciona CardBuilder admiten las funciones comunes de la interfaz de usuario que se describen a continuación. Consulta la documentación de los diseños individuales en CardBuilder.Layout para obtener una lista de las funciones compatibles con cada tipo de tarjeta.

Ícono de atribución

El ícono de atribución es un ícono opcional de 36 × 36 píxeles que aparece en la esquina inferior derecha de una tarjeta y a la derecha de la marca de tiempo. A fin de configurar este ícono, llama a CardBuilder.setAttributionIcon() para identificar tu aplicación, especialmente en tarjetas activas, de modo que el usuario pueda ver rápidamente la fuente de la información en esa tarjeta.

Indicador de pila

El indicador de pila, controlado por CardBuilder.showStackIndicator(), es un pliegue de esquina que aparece en la esquina superior derecha de una tarjeta. Úsalo como indicador visual de que tu tarjeta representa un paquete de otras tarjetas que el usuario puede presionar directamente.

View view = new CardBuilder(context, CardBuilder.Layout.TEXT)
    .setText("A stack indicator can be added to the corner of a card...")
    .setAttributionIcon(R.drawable.ic_smile)
    .showStackIndicator(true)
    .getView();

Diseños

En los siguientes ejemplos, se muestran los diseños disponibles con CardBuilder.

TEXT y TEXT_FIXED

El diseño CardBuilder.Layout.TEXT muestra texto sin márgenes con un mosaico de imagen opcional en segundo plano. El texto cambia de tamaño de forma dinámica para adaptarse mejor al espacio disponible. CardBuilder.Layout.TEXT_FIXED es similar, pero corrige su texto en un tamaño más pequeño.

View view1 = new CardBuilder(context, CardBuilder.Layout.TEXT)
    .setText("This is the TEXT layout. The text size will adjust dynamically.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .getView();
View view2 = new CardBuilder(context, CardBuilder.Layout.TEXT)
    .setText("You can also add images to the background of a TEXT card.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .addImage(R.drawable.image1)
    .addImage(R.drawable.image2)
    .addImage(R.drawable.image3)
    .addImage(R.drawable.image4)
    .addImage(R.drawable.image5)
    .getView();
View view3 = new CardBuilder(context, CardBuilder.Layout.TEXT_FIXED)
    .setText("This is the TEXT_FIXED layout. The text size is always the same.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .getView();

COLUMNS y COLUMNS_FIXED

El diseño CardBuilder.Layout.COLUMNS muestra un ícono o mosaico de imagen en el lado izquierdo de la tarjeta y el texto en el lado derecho. El tamaño del texto se ajusta de forma dinámica para adaptarse mejor al espacio disponible. Para mantener fijo el tamaño del texto, usa CardBuilder.Layout.COLUMNS_FIXED.

View view1 = new CardBuilder(context, CardBuilder.Layout.COLUMNS)
    .setText("This is the COLUMNS layout with dynamic text.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .addImage(R.drawable.image1)
    .addImage(R.drawable.image2)
    .addImage(R.drawable.image3)
    .addImage(R.drawable.image4)
    .addImage(R.drawable.image5)
    .getView();
View view2 = new CardBuilder(context, CardBuilder.Layout.COLUMNS)
    .setText("You can even put a centered icon on a COLUMNS card instead of a mosaic.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .setIcon(R.drawable.ic_wifi)
    .getView();
View view3 = new CardBuilder(context, CardBuilder.Layout.COLUMNS_FIXED)
    .setText("This is the COLUMNS_FIXED layout. The text size is always the same.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .addImage(R.drawable.image1)
    .addImage(R.drawable.image2)
    .addImage(R.drawable.image3)
    .addImage(R.drawable.image4)
    .addImage(R.drawable.image5)
    .getView();

CAPTION

El diseño CardBuilder.Layout.CAPTION tiene un mosaico de imagen en el fondo y un texto de leyenda breve alineado en la parte inferior de la tarjeta. También se puede colocar un ícono junto al subtítulo para representar, por ejemplo, la identidad de una persona asociada con el contenido de la tarjeta.

Figura 1: (imagen de fondo de photoeverywhere.co.uk, recortada)
View view1 = new CardBuilder(context, CardBuilder.Layout.CAPTION)
    .setText("The caption layout.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .addImage(R.drawable.beach)
    .setAttributionIcon(R.drawable.ic_smile)
    .getView();

View view2 = new CardBuilder(context, CardBuilder.Layout.CAPTION)
    .setText("The caption layout with an icon.")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .addImage(R.drawable.beach)
    .setIcon(R.drawable.ic_avatar)
    .setAttributionIcon(R.drawable.ic_smile)
    .getView();

TITLE

El diseño CardBuilder.Layout.TITLE tiene un mosaico de imágenes en el fondo con un título centrado y un ícono opcional en la parte inferior de la tarjeta. Este diseño a menudo se usa para representar objetivos de contactos o compartir. La posición y la marca de tiempo no son compatibles con este diseño.

View view = new CardBuilder(context, CardBuilder.Layout.TITLE)
    .setText("TITLE Card")
    .setIcon(R.drawable.ic_phone)
    .addImage(R.drawable.beach)
    .getView();

AUTHOR

Usa el diseño CardBuilder.Layout.AUTHOR para mostrar un mensaje o conversación donde el foco esté en el autor. Admite un mosaico de imágenes en segundo plano, un ícono que se usa como el avatar del autor, y un encabezado y subtítulo en el que puedes enumerar la información de identificación.

View view = new CardBuilder(context, CardBuilder.Layout.AUTHOR)
    .setText("The AUTHOR layout lets you display a message or conversation "
            + " with a focus on the author.")
    .setIcon(R.drawable.ic_avatar)
    .setHeading("Joe Lastname")
    .setSubheading("Mountain View, California")
    .setFootnote("This is the footnote")
    .setTimestamp("just now")
    .getView();

El diseño CardBuilder.Layout.MENU se parece a un menú estándar de Glass. Tiene un ícono y un título centrados, y una nota al pie opcional. Usa este diseño para las pantallas de confirmación (transición de "Borrado" a "Borrado") después de que el usuario seleccione un elemento de menú, por ejemplo. Si necesitas un menú real, debes usar un menú de opciones estándar.

View view = new CardBuilder(context, CardBuilder.Layout.MENU)
    .setText("MENU layout")
    .setIcon(R.drawable.ic_phone)
    .setFootnote("Optional menu description")
    .getView();

EMBED_INSIDE

El diseño CardBuilder.Layout.EMBED_INSIDE incorpora un XML de diseño personalizado de tu propio diseño en la plantilla estándar de la tarjeta de Glass. Esto te permite diseñar una IU personalizada para la aplicación y, aun así, tener la ubicación correcta de la nota al pie, la marca de tiempo, el ícono de atribución y el indicador de pila de una tarjeta, si es necesario.

Después de llamar a CardBuilder.getView(), usa findViewById() en el resultado para acceder a las vistas dentro del diseño incorporado. Del mismo modo, si llamas a CardBuilder.getRemoteViews(), puedes manipular las vistas del diseño incorporado si pasas sus ID directamente a los métodos set de RemoteViews.

View view = new CardBuilder(context, CardBuilder.Layout.EMBED_INSIDE)
    .setEmbeddedLayout(R.layout.food_table)
    .setFootnote("Foods you tracked")
    .setTimestamp("today")
    .getView();
TextView textView1 = (TextView) view.findViewById(R.id.text_view_1);
textView1.setText("Water");
// ...and so on

Para obtener un ejemplo más detallado, consulta el proyecto ApiDemo de GitHub.

ALERT

El diseño CardBuilder.Layout.ALERT contiene un ícono grande y centrado con un mensaje principal y una nota al pie. Usa este diseño en un Dialog para mostrar un mensaje informativo importante, una advertencia o un error en tu Glassware.

En el siguiente ejemplo, se muestra una implementación de AlertDialog y descarta la tarjeta y abre la configuración de Wi-Fi cuando el usuario presiona la tarjeta:

  1. Crea una clase que extienda Dialog.
  2. Crea la tarjeta usando CardBuilder con el diseño CardBuilder.Layout.ALERT y, luego, establece la vista de contenido con esta tarjeta.
  3. (Opcional) Crea un GestureDetector para controlar los gestos de los usuarios en esta tarjeta.

    public class AlertDialog extends Dialog {
    
        private final DialogInterface.OnClickListener mOnClickListener;
        private final AudioManager mAudioManager;
        private final GestureDetector mGestureDetector;
    
        /**
         * Handles the tap gesture to call the dialog's
         * onClickListener if one is provided.
         */
        private final GestureDetector.BaseListener mBaseListener =
            new GestureDetector.BaseListener() {
    
            @Override
            public boolean onGesture(Gesture gesture) {
                if (gesture == Gesture.TAP) {
                    mAudioManager.playSoundEffect(Sounds.TAP);
                    if (mOnClickListener != null) {
                        // Since Glass dialogs do not have buttons,
                        // the index passed to onClick is always 0.
                        mOnClickListener.onClick(AlertDialog.this, 0);
                    }
                    return true;
                }
                return false;
            }
        };
    
        public AlertDialog(Context context, int iconResId,
                           int textResId, int footnoteResId,
                           DialogInterface.OnClickListener onClickListener) {
            super(context);
    
            mOnClickListener = onClickListener;
            mAudioManager =
                (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
            mGestureDetector =
                new GestureDetector(context).setBaseListener(mBaseListener);
    
            setContentView(new CardBuilder(context, CardBuilder.Layout.ALERT)
                    .setIcon(iconResId)
                    .setText(textResId)
                    .setFootnote(footnoteResId)
                    .getView());
        }
    
        /** Overridden to let the gesture detector handle a possible tap event. */
        @Override
        public boolean onGenericMotionEvent(MotionEvent event) {
            return mGestureDetector.onMotionEvent(event)
                || super.onGenericMotionEvent(event);
        }
    }
    
  4. En tu actividad, implementa un OnClickListener para controlar los flujos adicionales cuando el usuario presione (opcional). Para obtener más información sobre cómo iniciar actividades de configuración, como Wi-Fi, consulta Configuración de inicio.

  5. Llama al constructor AlertDialog para mostrar la tarjeta de alerta.

    public class MyActivity extends Activity {
        ...
        private final DialogInterface.OnClickListener mOnClickListener =
                new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int button) {
                            // Open WiFi Settings
                            startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
                        }
                };
    
        @Override
        protected void onCreate(Bundle bundle) {
            ...
    
            new AlertDialog(context, R.drawable.ic_cloud_sad_150, R.string.alert_text,
                R.string.alert_footnote_text, mOnClickListener).show();
    
            ...
        }
    }
    

Diseños XML

A continuación, se incluyen dos diseños de tarjeta básicos que puedes usar si la clase CardBuilder no satisface tus necesidades.

Diseño principal

Este diseño define el relleno y el pie de página estándar de una tarjeta. Ingresa tus propias vistas en el RelativeLayout vacío.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <RelativeLayout
        android:id="@+id/body_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/glass_card_body_height"
        android:layout_marginLeft="@dimen/glass_card_margin"
        android:layout_marginTop="@dimen/glass_card_margin"
        android:layout_marginRight="@dimen/glass_card_margin"
        tools:ignore="UselessLeaf"
        >

        <!-- Put your widgets inside this RelativeLayout. -->

    </RelativeLayout>

    <LinearLayout
        android:id="@+id/footer_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|left"
        android:layout_marginLeft="@dimen/glass_card_margin"
        android:layout_marginBottom="@dimen/glass_card_footer_margin"
        android:layout_marginRight="@dimen/glass_card_margin"
        android:orientation="horizontal"
        >

        <!-- The footer view will grow to fit as much content as possible while the
             timestamp view keeps a fixed width. If the footer text is too long, it
             will be ellipsized with a 40px margin between it and the timestamp. -->

        <TextView
            android:id="@+id/footer"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ellipsize="end"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceSmall"
            />

        <TextView
            android:id="@+id/timestamp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/glass_card_margin"
            android:ellipsize="end"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceSmall"
            />

    </LinearLayout>

</FrameLayout>

Diseño de la columna izquierda

Esto define una columna izquierda de 240 px y una columna derecha de 400 px en forma de dos RelativeLayout en las que puedes colocar tus vistas.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    >

    <RelativeLayout
        android:id="@+id/left_column"
        android:layout_width="@dimen/glass_card_left_column_width"
        android:layout_height="match_parent"
        >

        <!-- Put widgets for the left column inside this RelativeLayout. -->

    </RelativeLayout>

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="@dimen/glass_card_body_height"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="@dimen/glass_card_two_column_margin"
        android:layout_marginRight="@dimen/glass_card_margin"
        android:layout_marginTop="@dimen/glass_card_margin"
        android:layout_toRightOf="@+id/left_column"
        tools:ignore="UselessLeaf"
        >

        <!-- Put widgets for the right column inside this RelativeLayout. -->

    </RelativeLayout>

    <LinearLayout
        android:id="@+id/footer_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_gravity="bottom|left"
        android:layout_marginBottom="@dimen/glass_card_footer_margin"
        android:layout_marginLeft="@dimen/glass_card_two_column_margin"
        android:layout_marginRight="@dimen/glass_card_margin"
        android:layout_toRightOf="@+id/left_column"
        android:orientation="horizontal"
        >

        <!--
             The footer view will grow to fit as much content as possible while the
             timestamp view keeps a fixed width. If the footer text is too long, it
             will be ellipsized with a 40px margin between it and the timestamp.
        -->

        <TextView
            android:id="@+id/footer"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ellipsize="end"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceSmall"
            />

        <TextView
            android:id="@+id/timestamp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/glass_card_margin"
            android:ellipsize="end"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceSmall"
            />

    </LinearLayout>

</RelativeLayout>

Dimensiones estándar

Usa este archivo junto con los diseños anteriores o con tus propios diseños para cumplir con el estilo estándar de Glass. Crea este archivo como res/values/dimens.xml en tu proyecto de Android.

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <!-- The recommended margin for the top, left, and right edges of a card. -->
    <dimen name="glass_card_margin">40px</dimen>

    <!-- The recommended margin between the bottom of the card and the footer. This is
         an adjusted value so that the baseline of the text in the footer sits 40px
         from the bottom of the card, matching the other margins. -->
    <dimen name="glass_card_footer_margin">33px</dimen>

    <!-- The recommended margin for the left column of the two-column card. -->
    <dimen name="glass_card_two_column_margin">30px</dimen>

    <!-- The maximum height of the body content inside a card. -->
    <dimen name="glass_card_body_height">240px</dimen>

    <!-- The width of the left column in the two-column layout. -->
    <dimen name="glass_card_left_column_width">240px</dimen>

</resources>