Como viste en los codelabs anteriores, Material es un sistema de diseño creado por Google con lineamientos, componentes y herramientas que respaldan las prácticas recomendadas para el diseño de interfaces de usuario. En este codelab, actualizarás la app de la calculadora de propinas (de los codelabs anteriores) para que ofrezca una experiencia del usuario más prolija, como se muestra en la siguiente captura de pantalla final. También probarás la app en algunas situaciones adicionales para garantizar que la experiencia del usuario sea lo más fluida posible.
Requisitos previos
- Conocer los conceptos básicos de los widgets de IU comunes, como
TextView
,ImageView
,Button
,EditText
,RadioButton
,RadioGroup
ySwitch
- Conocer los conceptos básicos de
ConstraintLayout
y el posicionamiento las vistas secundarias estableciendo restricciones - Sentirse a gusto con la modificación de diseños XML
- Conocer la diferencia entre las imágenes de mapas de bits y las interfaces dibujables en vector
- Poder establecer atributos de tema en un tema
- Poder activar el Tema oscuro en un dispositivo
- Haber modificado anteriormente el archivo
build.gradle
de la app para las dependencias del proyecto
Qué aprenderás
- Cómo usar los componentes de Material Design en tu app
- Cómo importar los íconos de material desde Image Asset Studio para usarlos en tu app
- Cómo crear y aplicar nuevos estilos
- Cómo configurar otros atributos de tema además del color
Qué compilarás
- Una app para calcular propinas prolija que sigue las prácticas recomendadas de la IU
Requisitos
- Una computadora que tenga Android Studio instalado
- El código de la app de Tip Time después de completar los codelabs anteriores
En los codelabs anteriores, creaste la app Tip Time, una app de calculadora de propinas que ofrece opciones para personalizar la propina. Actualmente, la IU de tu app se ve como la siguiente captura de pantalla. Las funciones se ejecutan correctamente, pero se parece más a un prototipo. Los campos no están alineados visualmente. Definitivamente, se pueden realizar mejoras en términos de estilo y espaciado más coherentes, así como con el uso de componentes de Material Design.
Los componentes de Material son widgets de IU comunes que facilitan la implementación de los estilos de Material en tu app. En la documentación, se proporciona información sobre cómo usar y personalizar los componentes de Material Design. Hay lineamientoss generales de Material Design para cada componente y una guía específica de la plataforma de Android para los componentes disponibles en Android. Los diagramas etiquetados te proporcionan suficiente información para recrear un componente en caso de que no exista en la plataforma que elegiste.
Si usas los componentes de Material, tu app funcionará de manera más uniforme junto con otras apps en el dispositivo del usuario. De esta manera, los patrones de la IU aprendidos en una app pueden transferirse a la siguiente. Por lo tanto, los usuarios podrán aprender a usar tu app mucho más rápido. Se recomienda a usar los componentes de Material siempre que sea posible (en lugar de los widgets que no son de Material). También son más flexibles y personalizables, como aprenderás en la próxima tarea.
La biblioteca de componentes de Material Design (MDC) debe incluirse como una dependencia en tu proyecto. Si usas Android Studio 4.1 o versiones posteriores, esta línea ya debería estar presente en tu proyecto de forma predeterminada. En el archivo build.gradle
de tu app, asegúrate de que esta dependencia se incluya en la versión más reciente de la biblioteca. Para obtener más detalles, consulta la página Comenzar del sitio de Material.
app/build.gradle
dependencies {
...
implementation 'com.google.android.material:material:<version>'
}
Campos de texto
En tu app de calculadora de propinas, en la parte superior del diseño, actualmente tienes un campo EditText
para los costos de servicio. Este campo EditText
funciona, pero no sigue los lineamientos recientes de Material Design sobre cómo se ven y se comportan los campos de texto.
Comienza por aprender sobre cualquier componente nuevo que desees utilizar en el sitio de Material. En la guía sobre Campos de texto, hay dos tipos de campos de texto:
Campo de texto completado
Campo de texto con contorno
Para crear un campo de texto como se muestra más arriba, usa un TextInputLayout
con un TextInputEditText
adjunto de la biblioteca MDC. El campo de texto de Material se puede personalizar fácilmente para lo siguiente:
- Mostrar texto de entrada o una etiqueta que siempre está visible
- Mostrar un ícono en el campo de texto
- Mostrar mensajes de error o del asistente
En la primera tarea de este codelab, reemplazarás el costo de servicio EditText
por un campo de texto de Material (que está compuesto por un TextInputLayout
y un TextInputEditText
).
- Con la app Tip Time abierta en Android Studio, ve al archivo de diseño
activity_main.xml
. Debería contener unConstraintLayout
con el diseño de la calculadora de propinas. - Para ver un ejemplo de la apariencia del XML para un campo de texto de Material, vuelve a la guía de Android sobre campos de texto. Deberías ver fragmentos como este:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
- Después de ver este ejemplo, inserta un campo de texto de material como el primer elemento secundario de
ConstraintLayout
(antes del campoEditText
). Quitarás el campoEditText
en un paso posterior.
Puedes escribir esto en Android Studio y usar el autocompletado para facilitar este proceso. También puedes copiar el archivo XML de ejemplo de la página de documentación y pegarlo en tu diseño de esta manera. Observa cómo TextInputLayout
tiene una vista secundaria, TextInputEditText
. Recuerda que la elipsis (...) se usa para abreviar fragmentos, de modo que puedas centrarte en las líneas del XML que realmente cambiaron.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/cost_of_service" ... />
...
Se espera que veas errores en el elemento TextInputLayout
. Aún no restringiste correctamente esta vista en la ConstraintLayout
superior. Tampoco se reconoce el recurso de strings. Solucionarás estos errores en los próximos pasos.
- Agrega restricciones verticales y horizontales al campo de texto para ubicarlo correctamente en el elemento
ConstraintLayout
superior. Como aún no borraste elEditText
, corta y pega los siguientes atributos deEditText
y colócalos enTextInputLayout
: las restricciones, el ID de recursocost_of_service
, el ancho del diseño de160dp
, la altura del diseño dewrap_content
y el texto de la sugerencia@string/cost_of_service
.
...
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:hint="@string/cost_of_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
...
Es posible que veas un error que indica que el ID de cost_of_service
es igual al ID de recurso de EditText
, pero puedes ignorar este error por el momento. (EditText
se quitará en algunos pasos).
- A continuación, asegúrate de que el elemento
TextInputEditText
tenga todos los atributos adecuados. Corta y pega el tipo de entrada deEditText
enTextInputEditText.
y cambia el ID de recurso del elementoTextInputEditText
acost_of_service_edit_text.
.
<com.google.android.material.textfield.TextInputLayout ... >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/cost_of_service_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
El ancho de match_parent
y la altura de wrap_content
son correctos. Cuando se define un ancho de match_parent
, el TextInputEditText
tiene el mismo ancho que su elemento superior TextInputLayout
, que es 160dp
.
- Ahora que copiaste toda la información relevante de
EditText
, borra el elementoEditText
del diseño. - En la vista Design de tu diseño, debería aparecer esta vista previa. El campo de costo de servicio ahora se parece al campo de texto de Material.
- Aún no puedes ejecutar la app porque hay un error en tu archivo
MainActivity.kt
en el métodocalculateTip()
. Recuerda de un codelab anterior que con la vinculación de vista habilitada para tu proyecto, Android crea propiedades en un objeto de vinculación basadas en el nombre del ID de recurso. El campo sobre el cual recuperamos el costo de servicio cambió en el diseño XML, por lo que el código Kotlin se debe actualizar en consecuencia.
Ahora, recuperarás la entrada del usuario del elemento TextInputEditText
con el ID de recurso cost_of_service_edit_text
. En MainActivity
, usa binding.costOfServiceEditText
para acceder a la string de texto almacenada en ella. El resto del método calculateTip()
puede mantenerse igual.
private fun calculateTip() {
// Get the decimal value from the cost of service text field
val stringInTextField = binding.costOfServiceEditText.text.toString()
val cost = stringInTextField.toDoubleOrNull()
...
}
- ¡Muy bien! Ahora ejecuta la app y comprueba que aún funcione. Observa que la etiqueta "Cost of Service" ahora aparecerá encima de tus datos a medida que escribes. La sugerencia se debe calcular como se espera.
Interruptores
En los lineamientos de Material Design, también encontrarás orientación sobre los interrupciones. Un botón es un widget en el que puedes activar o desactivar una configuración.
- Consulta la guía de Android sobre los interruptores de Material. Aprenderás sobre el widget
SwitchMaterial
(de la biblioteca MDC), que proporciona estilos de Material para interruptores. Si sigues desplazándote por la guía, verás algunos ejemplos de XML. - Para usar
SwitchMaterial
, debes especificarSwitchMaterial
de forma explícita en tu diseño y usar el nombre de ruta de acceso completamente calificado.
En el diseño de activity_main.xml
, cambia la etiqueta XML de Switch
a com.google.android.material.switchmaterial.SwitchMaterial.
.
...
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content" ... />
...
- Ejecuta la app para verificar que siga compilando. No ocurre ningún cambio visible en la app. Sin embargo, una ventaja de usar
SwitchMaterial
de la biblioteca MDC (en lugar deSwitch
desde la plataforma de Android) es que, cuando se actualiza la implementación de la biblioteca paraSwitchMaterial
(p. ej., se modifican los lineamientos de Material Design). recibirás el widget actualizado de forma gratuita sin necesidad de realizar cambios. Esto ayuda a que tu app esté preparada para el futuro.
En este punto, vimos dos ejemplos de cómo la IU puede beneficiarse de usar componentes de Material Design listos para usar y cómo mejora tu app de acuerdo con los lineamientos de Material. Recuerda que siempre puedes explorar otros componentes de Material Design que se proporcionan en Android en este sitio.
Los íconos son símbolos que ayudan a los usuarios a comprender la interfaz de usuario comunicando de forma visual la función prevista. Suelen inspirarse en los objetos del mundo físico que se espera que haya experimentado un usuario. Por lo general, el diseño del ícono reduce el nivel de detalle al mínimo necesario para que le resulte familiar al usuario. Por ejemplo, un lápiz en el mundo físico se usa para escribirlo, de modo que el equivalente de ícono generalmente indica crear, agregar o editar un elemento.
Foto de Angelina Litvin en Unsplash |
A veces, los íconos están vinculados a objetos de mundo físico obsoletos, como es el caso del ícono del disquete. Este ícono es la indicación aproximada de guardar un archivo o registro de base de datos. Sin embargo, aunque los disquetes se popularizaron en la década de 1970, dejaron de ser comunes después de 2000. Sin embargo, el hecho de que se siga usando en la actualidad muestra cómo un elemento visual fuerte puede trascender la vida útil de su forma física.
Foto de Vincent Botta en Unsplash |
Cómo representar íconos de tu app
En el caso de los íconos de tu app, en lugar de proporcionar diferentes versiones de una imagen de mapa de bits para diferentes densidades de pantalla, se recomienda usar elementos de diseño vectoriales. Los elementos de diseño vectoriales se representan como archivos XML que almacenan las instrucciones sobre cómo crear una imagen en lugar de guardar los píxeles reales que conforman esa imagen. Los elementos de diseño vectoriales pueden aumentarse o reducirse sin que se pierda la calidad visual ni el tamaño del archivo.
Íconos proporcionados
Material Design proporciona una serie de íconos organizados en categorías comunes para la mayoría de tus necesidades. Consulta la lista de íconos.
Estos íconos también se pueden dibujar con uno de cinco temas (Filled, Outlined, Rounded, Two-Tone y Sharp) y se pueden agregar a tonos con colores.
Filled | Outlined | Rounded | Two-Tone | Sharp |
Cómo agregar íconos
En esta tarea, agregarás tres íconos de elementos de diseño vectoriales a la app:
- Ícono junto al campo de texto de costo de servicio
- Ícono junto a la pregunta de servicio
- Ícono junto a la solicitud de redondeo
A continuación, se muestra una captura de pantalla de la versión final de la app. Una vez que agregues los íconos, modificarás el diseño para que se ajuste a su ubicación. Observa cómo los campos y el botón Calcular se desplazan hacia la derecha, con la incorporación de los íconos.
Cómo agregar elementos de la interfaz dibujable en vector
Puedes crear estos íconos como elementos de diseño vectoriales directamente desde Asset Studio en Android Studio.
- Abre la pestaña Resource Manager ubicada a la izquierda de la ventana de la aplicación.
- Haz clic en el ícono + y selecciona Vector Asset.
- En Asset Type, asegúrate de que el botón de selección etiquetado Clip Art esté seleccionado.
- Haz clic en el botón junto a Clip Art: para seleccionar otra imagen prediseñada. En el mensaje que aparece, escribe "call made" en la ventana. Utilizarás este ícono de flecha para la opción de redondeo de propina. Selecciónala y haz clic en OK.
- Cambia el nombre del ícono por
ic_round_up
. (Se recomienda usar el prefijo ic_ cuando nombres los archivos de íconos). Puedes dejar **Size** como 24 dp x 24 dp y **Color** como black 000000. - Presiona Siguiente.
- Acepta la ubicación predeterminada del directorio y haz clic en Finish.
- Repite los pasos 2 a 7 para los otros dos íconos:
- Ícono de pregunta de servicio: Busca el ícono de "servicio a la habitación" y guárdalo como
ic_service
. - Ícono de costo de servicio: Busca el ícono de "tienda" y guárdalo como
ic_store
.
- Cuando termines, Resource Manager se verá como en la siguiente captura de pantalla. También verás estos tres elementos de diseño vectoriales (
ic_round_up
,ic_service
yic_store
) en tu carpetares/drawable
.
Compatibilidad con versiones anteriores de Android
Acabas de agregar elementos de diseño vectoriales a tu app, pero es importante tener en cuenta que la compatibilidad con estos elementos en la plataforma de Android no se agregó hasta Android 5.0 (nivel de API 21).
Según la configuración de tu proyecto, la versión mínima del SDK para la app de Tip Time es la API 19. Esto significa que la app puede ejecutarse en dispositivos Android que ejecutan la versión 19 o versiones posteriores de la plataforma de Android.
Para que tu app funcione en estas versiones anteriores de Android (lo que se conoce como retrocompatibilidad), agrega el elemento vectorDrawables
al archivo build.gradle
de tu app. Esto te permite usar elementos de diseño vectoriales en versiones de la plataforma anteriores a la API 21, en lugar de realizar la conversión a PNG cuando se crea el proyecto. Obtén más detalles aquí.
app/build.gradle
android {
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
Con tu proyecto configurado correctamente, ahora podrás pasar de agregar los íconos al diseño.
Cómo insertar íconos y posicionar elementos
Usarás ImageViews
para mostrar los íconos en la app. Así es como aparecerá tu IU final.
- Abre el diseño de
activity_main.xml
. - Primero, posiciona el ícono de la tienda junto al campo de texto de costo de servicio. Inserta un nuevo objeto
ImageView
como el primer elemento secundario deConstraintLayout
, antes deTextInputLayout
.
<androidx.constraintlayout.widget.ConstraintLayout
...>
<ImageView
android:layout_width=""
android:layout_height=""
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
- Configura los atributos adecuados en
ImageView
para mantener el ícono deic_store
. Configura el ID comoicon_cost_of_service
. Establece el atributoapp:srcCompat
en el recurso de elemento de diseño@drawable/ic_store
y obtendrás una vista previa del ícono junto a esa línea de XML. También configuraandroid:importantForAccessibility="no"
, ya que esta imagen solo se usa con fines decorativos.
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store" />
Se espera que haya un error en el ImageView
, ya que la vista aún no está restringida. A continuación, corregirás este problema.
- Coloca el
icon_cost_of_service
en dos pasos. Primero, agrega restricciones al elementoImageView
(este paso) y, luego, actualiza las restricciones en elTextInputLayout
junto a él (paso 5). En este diagrama, se muestra cómo se deben configurar las restricciones.
En ImageView
, deseas que el borde inicial esté restringido al borde inicial de la vista superior (app:layout_constraintStart_toStartOf="parent"
).
El ícono aparece centrado verticalmente en comparación con el campo de texto que está al lado, así que restringe la parte superior de este ImageView
(layout_constraintTop_toTopOf
) a la parte superior del campo de texto. Limita la parte inferior de este ImageView
(layout_constraintBottom_toBottomOf
) al final del campo de texto. Para hacer referencia al campo de texto, usa el ID de recurso @id/cost_of_service
. El comportamiento predeterminado es que cuando dos restricciones se aplican a un widget en la misma dimensión (como una restricción superior y inferior), las restricciones se aplican de la misma manera. El resultado es que el ícono se centre verticalmente en relación con el campo de costo de servicio.
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/cost_of_service"
app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />
El ícono y el campo de texto aún se superponen en la vista Design. Esto se corregirá en el siguiente paso.
- Antes de agregar el ícono, el campo de texto se ubicó al inicio del elemento principal. Ahora se debe desplazar hacia la derecha. Actualiza las restricciones del campo de texto
cost_of_service
en relación conicon_cost_of_service
.
El borde inicial de TextInputLayout
debe limitarse al borde final de ImageView
(@id/icon_cost_of_service
). Para agregar un poco de espacio entre las dos vistas, agrega un margen de inicio de 16dp
en el TextInputLayout
.
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">
<com.google.android.material.textfield.TextInputEditText ... />
</com.google.android.material.textfield.TextInputLayout>
Después de todos estos cambios, el ícono debe estar ubicado correctamente junto al campo de texto.
- Luego, inserta el ícono de campana de servicio junto al mensaje "¿Cómo fue el servicio?"
TextView
. Aunque puedes declarar elImageView
en cualquier lugar deConstraintLayout
, tu diseño XML será más fácil de leer si insertas el nuevoImageView
en el diseño XML después deTextInputLayout
, pero antes delservice_question
TextView
.
Para la nueva ImageView
, asígnale un ID de recurso de @+id/icon_service_question
. Establece las restricciones apropiadas en ImageView
y la pregunta de servicio TextView
.
También agrega un margen superior de 16dp
a service_question TextView
para que haya más espacio vertical entre la pregunta de servicio y el campo de texto de servicio más arriba.
...
<ImageView
android:id="@+id/icon_service_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/service_question"
app:layout_constraintBottom_toBottomOf="@id/service_question" />
<TextView
android:id="@+id/service_question"
...
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/cost_of_service"
app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>
...
- En este punto, la vista Design debería verse así. El campo de costo de servicio y la pregunta de servicio (y sus respectivos íconos) se ven geniales, pero los botones de selección ahora salen de la ubicación. No están alineados verticalmente con el contenido anterior.
- Mejora la posición de los botones de selección moviéndolos hacia la derecha, debajo de la pregunta de servicio. Eso significa actualizar una restricción
RadioGroup
. Restringe el borde inicial deRadioGroup
al borde inicial deservice_question
TextView
. Todos los demás atributos deRadioGroup
pueden permanecer iguales.
...
<RadioGroup
android:id="@+id/tip_options"
...
app:layout_constraintStart_toStartOf="@id/service_question">
...
- Para continuar, agrega el ícono
ic_round_up
al diseño junto al interruptor de la opción de redondeo de propina. Intenta hacer esto por tu cuenta, y si no sabes cómo hacerlo, consulta el siguiente archivo XML. Puedes asignar al nuevoImageView
un ID de recurso deicon_round_up
. - En el XML de diseño, inserta una
ImageView
nueva después delRadioGroup
, pero antes del widgetSwitchMaterial
. - Asigna el
ImageView
del ID de recurso deicon_round_up
y establecesrcCompat
en el elemento de diseño del ícono@drawable/ic_round_up
. Restringe el inicio delImageView
al inicio del elemento principal y también centra el ícono verticalmente con relación alSwitchMaterial
. - Actualiza
SwitchMaterial
para que aparezca junto al ícono y tenga un margen de inicio de16dp
. Así debería verse el XML resultante paraicon_round_up
yround_up_switch
.
...
<ImageView
android:id="@+id/icon_round_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_round_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/round_up_switch"
app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_round_up" />
...
- La vista Design debería verse así. Los tres íconos están bien posicionados.
- Si comparas esto con la captura de pantalla final de la app, notarás que el botón de cálculo también se desplaza de manera vertical para alinearlo con el costo de servicio, la pregunta de servicio, las opciones del botón de selección y la pregunta de redondeo de propina. Para lograr esto, restringe el inicio del botón de cálculo al inicio del
round_up_switch
. Además, agrega un8dp
de margen vertical entre el botón de cálculo y el interruptor que aparece sobre él.
...
<Button
android:id="@+id/calculate_button"
...
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@id/round_up_switch" />
...
- Por último, posiciona
tip_result
agregando8dp
de margen superior aTextView
.
...
<TextView
android:id="@+id/tip_result"
...
android:layout_marginTop="8dp" />
...
- Fueron muchos pasos. Felicitaciones por completarlos. Hay que prestar mucha atención a los detalles para que los elementos se alineen correctamente en el diseño, pero hace que el resultado final se vea mucho mejor. Ejecuta la app. Debería verse como en la siguiente captura de pantalla. Los elementos no aparecen amontonados, ya que se los alineó de forma vertical y se aumentó el espacio entre ellos.
Aún no has terminado. Quizás hayas notado que el tamaño de fuente y el color de la pregunta de servicio y el importe de la propina no coinciden con el texto de los botones de selección y el interruptor. Hagamos que sean coherentes en la siguiente tarea mediante el uso de estilos y temas.
Un estilo es una colección de valores de atributos de vista para un solo tipo de widget. Por ejemplo, un estilo TextView
puede especificar el color y el tamaño de fuente, el color de fondo, etc. Al extraer estos atributos en un estilo, puedes aplicar el estilo a varias vistas en el diseño de manera sencilla y mantenerlo en un solo lugar.
En esta tarea, primero crearás estilos para la vista de texto, el botón de selección y el cambio de widgets.
Cómo crear estilos
- Crea un archivo nuevo llamado
styles.xml
en el directorio res > values, si aún no existe. Para crearlo, haz clic con el botón derecho en el directorio values y selecciona New > Values Resource File. Asígnale el nombrestyles.xml
. El nuevo archivo tendrá el siguiente contenido.
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
- Crea un nuevo estilo
TextView
para que el texto parezca coherente en toda la app. Define el estilo una vez enstyles.xml
y luego puedes aplicarlo a todos losTextViews
del diseño. Si bien puedes definir un estilo desde cero, puedes extenderte desde un estiloTextView
existente de la biblioteca de MDC.
Cuando aplicas estilo a un componente, generalmente debes extender desde un estilo superior del tipo de widget que usas. Esto es importante por dos motivos. Primero, se asegura de que todos los valores predeterminados importantes estén configurados en tu componente y, en segundo lugar, tu estilo seguirá heredando los cambios futuros que pertenezcan a ese estilo superior.
Puedes asignarle un nombre a tu estilo, pero existe una convención recomendada. Si la heredas de un estilo de Material superior, asigna un nombre a tu estilo de forma paralela; para ello, sustituye MaterialComponents
por el nombre de tu app (TipTime
). Esto traslada los cambios a su propio espacio de nombres, lo que elimina la posibilidad de conflictos futuros cuando los componentes de Material presenten nuevos estilos. Ejemplo:
Nombre de tu estilo: Widget.TipTime.TextView
Hereda del estilo superior: Widget.MaterialComponents.TextView
Agrégalo a tu archivo styles.xml
entre las etiquetas de apertura y cierre de resources
.
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
- Configura tu estilo de
TextView
para que anule los siguientes atributos:android:minHeight,android:gravity,
yandroid:textAppearance.
android:minHeight
establece una altura mínima de 48 dp en TextView
. La altura más baja de cualquier fila debe ser de 48 dp según los lineamientos de Material Design.
Puedes centrar el texto en TextView
verticalmente si configuras el atributo android:gravity
. (Consulta la captura de pantalla que aparece a continuación). La gravedad controla cómo se posicionará el contenido dentro de una vista. Como el contenido de texto real no ocupa toda la altura de 48 dp, el valor center_vertical
centra el texto dentro de TextView
verticalmente (pero no cambia su posición horizontal). Otros valores de gravedad posibles son center
, center_horizontal
, top
y bottom
. No dudes en probar los otros valores de gravedad para ver el efecto en el texto.
Establece el valor del atributo de apariencia del texto en ?attr/textAppearanceBody1
. TextAppearance es un conjunto de estilos prediseñados para el tamaño del texto, las fuentes y otras propiedades del texto. Para descubrir otras apariencias de texto posibles que proporciona Material, consulta esta lista de los tipos de escala.
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- Para aplicar el estilo de
Widget.TipTime.TextView
a laTextView
deservice_question
, agrega un atributo de estilo en cadaTextView
deactivity_main.xml
.
<TextView
android:id="@+id/service_question"
style="@style/Widget.TipTime.TextView"
... />
Antes del estilo, TextView
se veía así con el tamaño de fuente pequeño y el color de fuente gris:
Después de agregar el estilo, TextView
se ve de la siguiente manera: Ahora este TextView
se ve más coherente con el resto del diseño.
- Aplica ese mismo estilo
Widget.TipTime.TextView
atip_result
TextView
.
<TextView
android:id="@+id/tip_result"
style="@style/Widget.TipTime.TextView"
... />
- El mismo estilo de texto debe aplicarse a la etiqueta de texto del interruptor. Sin embargo, no puedes establecer un estilo
TextView
en un widgetSwitchMaterial
. Los estilos deTextView
solo se pueden aplicar enTextViews
. Por lo tanto, crea un nuevo estilo para el cambio. Los atributos son los mismos en términos deminHeight
,gravity
ytextAppearance
. A continuación, se explica qué es el nombre del estilo y el elemento superior porque ahora se hereda del estiloSwitch
de la biblioteca MDC. Tu nombre de estilo también debe reflejar el nombre del estilo superior.
Tu nombre de estilo: Widget.TipTime.CompoundButton.Switch
. Hereda del estilo superior: Widget.MaterialComponents.CompoundButton.Switch
.
<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
También puedes indicar atributos adicionales específicos de los cambios en este estilo, pero no es necesario que lo hagas en tu app.
- El texto del botón de selección es el último lugar donde deseas asegurarte de que el texto aparezca visualmente coherente. No puedes aplicar un estilo
TextView
o un estiloSwitch
a un widgetRadioButton
. En su lugar, debes crear un nuevo estilo para los botones de selección. Puedes extender el estiloRadioButton
de la biblioteca de MDC.
Cuando estés creando este estilo, agrega también un relleno entre el texto del botón de selección y la imagen visual del círculo. paddingStart
es un nuevo atributo que no usaste aún. El relleno es la cantidad de espacio entre el contenido de una vista y los límites de la vista. El atributo paddingStart
establece el relleno solo al comienzo del componente. Consulta la diferencia entre 0 dp y 8 dp de paddingStart
en un botón de selección.
<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
<item name="android:paddingStart">8dp</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- (Opcional) Crea un archivo
dimens.xml
para mejorar la administración de los valores utilizados con frecuencia. Puedes crear el archivo de la misma manera que lo hiciste para el archivostyles.xml
anterior. Selecciona el directorio de valores y haz clic con el botón derecho y selecciona New > Values Resource File.
En esta app pequeña, repetiste la configuración de altura mínima dos veces. Eso es manejable por el momento, pero se descontrolaría rápidamente si tuviéramos 4, 6, 10 o más componentes que comparten ese valor. Recordar cambiarlos todos individualmente es tedioso y propenso a errores. Puedes crear otro archivo de recursos útil en res > values llamado dimens.xml
que contenga las dimensiones comunes a las que puedas asignar un nombre. Debido a la estandarización de los valores comunes como dimensiones con nombre, facilitamos la administración de nuestra app. Tip Time es pequeña, por lo que no la utilizaremos fuera de este paso opcional. Sin embargo, con apps más complejas en un entorno de producción en el que podrías trabajar con un equipo de diseño, dimens.xml
te permitirá cambiar con facilidad esos valores con más frecuencia.
dimens.xml
<resources>
<dimen name="min_text_height">48dp</dimen>
</resources>
Deberías actualizar el archivo styles.xml
para que use @dimen/min_text_height
en lugar de 48dp
directamente.
...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">@dimen/min_text_height</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...
Cómo agregar estos estilos a tus temas
Tal vez hayas notado que aún no aplicaste los nuevos estilos RadioButton
y Switch
a los widgets respectivos. Esto se debe a que usarás atributos de tema para configurar radioButtonStyle
y switchStyle
en el tema de la app. Repasemos qué es un tema.
Un tema es una colección de recursos con nombre (llamados atributos de tema) a los que se puede hacer referencia más adelante en estilos, diseños, etc. Puedes especificar un tema para una app, una actividad o una jerarquía de vistas en general, no solo una View.
individual. Anteriormente, modificaste el tema de la app en themes.xml
mediante la configuración de atributos de tema como colorPrimary
y colorSecondary
, que se usan en toda la app y sus componentes.
radioButtonStyle
y switchStyle
son otros atributos de tema que puedes establecer. Los recursos de estilo que proporciones para estos atributos de tema se aplicarán a cada botón de selección y a cada interruptor de la jerarquía de vistas a la que se aplique el tema.
También hay un atributo de tema para textInputStyle
, en el que el recurso de estilo especificado se aplicará a todos los campos de entrada de texto dentro de la app. Para que TextInputLayout
aparezca como un campo de texto detallado (como se muestra en los lineamientos de Material Design), hay un estilo OutlinedBox
definido en la biblioteca de MDC como Widget.MaterialComponents.TextInputLayout.OutlinedBox
. Este es el estilo que usarás.
- Modifica el archivo
themes.xml
para que el tema haga referencia a los estilos deseados. La configuración de un atributo de tema se realiza de la misma manera en la que declaraste los atributos de temacolorPrimary
ycolorSecondary
en un codelab anterior. Sin embargo, esta vez, los atributos de tema relevantes sontextInputStyle
,radioButtonStyle
yswitchStyle
. Usarás los estilos que creaste anteriormente paraRadioButton
ySwitch
, junto con el estilo del campo de texto materialOutlinedBox
.
Copia lo siguiente en res/values/themes.xml
en la etiqueta de estilo del tema de tu app.
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
- Tu archivo
res/values/themes.xml
debería verse de la siguiente manera. Puedes agregar comentarios en el XML si lo deseas (indicado por<!-
y-->
).
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- Radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- Switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- Asegúrate de realizar los mismos cambios en el Tema oscuro en themes.xml (night). Tu archivo
res/values-night/themes.xml
debería tener el siguiente aspecto:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Application theme for dark theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- For radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- For switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- Ejecuta la app y observa los cambios. El estilo de
OutlinedBox
se ve mucho mejor para el campo de texto y ahora todo el texto parece coherente.
Cuando estés cerca de completar tu app, debes probarla no solo con el flujo de trabajo previsto, sino también en otras situaciones de los usuarios. Tal vez descubras que algunos pequeños cambios en el código pueden mejorar la experiencia del usuario de forma considerable.
Cómo rotar el dispositivo
- Rota tu dispositivo al modo de paisaje. Primero, es posible que debas habilitar la configuración de Rotación automática. (Se encuentra en la Configuración rápida del dispositivo o en Configuración > Pantalla > Avanzada > Girar la pantalla automáticamente).
En el emulador, puedes usar sus opciones (ubicadas en la parte superior derecha, junto al dispositivo) para rotar la pantalla hacia la derecha o la izquierda.
- Notarás que se truncarán algunos de los componentes de la IU, incluido el botón Calcular. Esto evita que uses la app.
- Para solucionar este error, agrega un
ScrollView
alrededor delConstraintLayout
. El código XML se verá de la siguiente manera.
<ScrollView
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_height="match_parent"
android:layout_width="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
...
</ConstraintLayout>
</ScrollView>
- Vuelve a ejecutar y probar la app. Cuando giras el dispositivo al modo de paisaje, deberías poder desplazar la IU para acceder al botón Calcular y ver el resultado. Esta corrección no solo es útil en el modo de paisaje, sino también en otros dispositivos Android que pueden tener dimensiones diferentes. Ahora, independientemente del tamaño de la pantalla del dispositivo, el usuario puede desplazarse por el diseño.
Cómo ocultar teclado con la tecla Intro
Es posible que notes que, después de ingresar a un costo de servicio, el teclado permanece activo. Es un poco complicado ocultar manualmente el teclado cada vez para acceder mejor al botón de cálculo. En su lugar, haz que el teclado se oculte automáticamente cuando presionas la tecla Intro.
Para el campo de texto, puedes definir un objeto de escucha de claves para responder a los eventos cuando se presionan ciertas teclas. Cada opción de entrada posible en un teclado tiene un código de clave asociado, incluida la tecla Enter
. Ten en cuenta que un teclado en pantalla también se conoce como un teclado liviano, en lugar de uno físico.
En esta tarea, configura un objeto de escucha de claves en el campo de texto para escuchar cuando se presione la tecla Enter
. Cuando se detecte ese evento, oculta el teclado.
- Copia este método auxiliar y pégalo en tu clase
MainActivity
. Puedes insertarlo justo antes de la llave de cierre de la claseMainActivity
.handleKeyEvent()
es una función auxiliar privada que oculta el teclado en pantalla si el parámetro de entradakeyCode
es igual aKeyEvent.
KEYCODE_ENTER
. InputMethodManager controla si se muestra, se oculta un teclado de software y permite que el usuario elija qué teclado de software se muestra. El método muestra el valor "true" si el evento clave se manejó y muestra el valor "false" en caso contrario.
MainActivity.kt
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
- Ahora adjunta un objeto de escucha de claves en el widget
TextInputEditText
. Recuerda que puedes acceder al widget deTextInputEditText
mediante el objeto de vinculación comobinding.costOfServiceEditText.
Llama al método setOnKeyListener()
en el costOfServiceEditText
y pasa un OnKeyListener
. Esto es similar a cómo configuras un objeto de escucha de clics en el botón calcular en la app con binding.calculateButton.setOnClickListener { calculateTip() }.
.
El código para configurar un objeto de escucha de claves en una vista es un poco más complejo, pero la idea general es que OnKeyListener
tiene un método onKey()
que se activa cuando se presiona una tecla. El método onKey()
toma 3 argumentos de entrada: la vista, el código de la clave que se presionó y un evento clave (que no usarás, de modo que puedes llamar a "_
"). Cuando se llama al método onKey()
, debes llamar a tu método handleKeyEvent()
y pasar los objetos de vista de código y vista. La sintaxis para escribir esto es: view, keyCode, _ -> handleKeyEvent(view, keyCode).
Esto se denomina una expresión lambda, pero obtendrás más información sobre lambdas en una unidad posterior.
Agrega el código para configurar el objeto de escucha de claves en el campo de texto dentro del método onCreate()
de la actividad. Esto es porque deseas que el objeto de escucha de claves se adjunte tan pronto como se cree el diseño y antes de que el usuario comience a interactuar con la actividad.
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
}
}
- Prueba que tus cambios nuevos funcionen. Ejecuta la app e ingresa un costo de servicio. Presiona la tecla Intro en el teclado y el teclado en pantalla debería ocultarse.
Cómo probar tu app con TalkBack habilitado
Como estuviste aprendiendo a lo largo de este curso, deseas compilar apps que sean accesibles para tantos usuarios como sea posible. Algunos usuarios pueden usar Talkback para acceder a tu app y navegar por ella. TalkBack es el lector de pantalla de Google incluido en los dispositivos Android. Proporciona comentarios por voz que permiten usar un dispositivo sin mirar la pantalla.
Con TalkBack habilitado, asegúrate de que un usuario pueda completar el caso de uso de cómo calcular la propina en tu app.
- Para habilitar TalkBack en tu dispositivo, sigue estas instrucciones.
- Regresa a la app de Tip Time.
- Explora tu app con Talkback usando estas instrucciones. Desliza el dedo hacia la derecha para navegar por los elementos de la pantalla en secuencia y desliza el dedo hacia la izquierda para ir en la dirección opuesta. Presiona dos veces en cualquier lugar para realizar una selección. Verifica que puedas llegar a todos los elementos de la app con gestos de deslizamiento.
- Asegúrate de que un usuario de TalkBack pueda navegar a cada elemento de la pantalla, ingresar un costo de servicio, cambiar las opciones de propina, calcular la propina y escuchar la propina. Recuerda que no se proporcionan comentarios por voz para los íconos, ya que los marcaste como
importantForAccessibility="no"
.
Para obtener más información sobre cómo mejorar la accesibilidad de tu app, consulta estos principios.
(Opcional) Cómo ajustar el tono de los elementos de diseño vectoriales
En esta tarea opcional, aplicarás tonos para los íconos según el color principal del tema, de modo que los íconos se vean de manera diferente en el tema claro u oscuro (como se muestra a continuación). Este cambio es una buena adición a tu IU para que los íconos parezcan más coherentes con el tema de la app.
Como mencionamos antes, una de las ventajas de VectorDrawables
frente a las imágenes de mapa de bits es la capacidad de ajustarlas y ajustarles el tono. A continuación, se detalla el XML que representa el ícono de campana. Hay dos atributos de color específicos para tener en cuenta: android:tint
y android:fillColor
.
ic_service.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>
Si hay un tono presente, se anulará cualquier directiva de fillColor
para el elemento de diseño. En este caso, el color blanco se anula con el atributo del tema colorControlNormal
. colorControlNormal
es el color de "normal" (estado no seleccionado/desactivado) de un widget. Esto es un color gris.
Una mejora visual que podemos realizar en la app es cambiar el tono del elemento de diseño según el color principal del tema de la app. En el tema claro, el ícono aparecerá como @color/green
, mientras que en el Tema oscuro, el ícono aparecerá como @color/green_light
, que es el ?attr/colorPrimary
. El ajuste de tono del elemento de diseño según el color principal del tema de la app puede hacer que los elementos del diseño se vean más unificados y coherentes. Esto también nos evita tener que duplicar el conjunto de íconos para el tema claro y el Tema oscuro. Solo hay un conjunto de elementos de diseño vectoriales, y el tono cambiará en el atributo del tema colorPrimary
.
- Cambia el valor del atributo
android:tint
enic_service.xml
android:tint="?attr/colorPrimary"
En Android Studio, el ícono ahora tiene el tono adecuado.
El valor al que apunta el atributo colorPrimary
del tema variará según el tema claro o el oscuro.
- Repite lo mismo para cambiar el tono en los otros elementos de diseño vectoriales.
ic_store.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
ic_round_up.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
- Ejecuta la app. Verifica que los íconos se vean de manera diferente en los temas claros y oscuros.
- Como último paso de limpieza, recuerda cambiar el formato de todos los archivos de código XML y Kotlin en tu app.
Felicitaciones. Completaste la app de la calculadora de propinas. Felicitaciones por lo que compilaste. Esperamos que este sea un punto de partida para que desarrolles apps aún más hermosas y funcionales.
El código de solución para este codelab se encuentra en el repositorio de GitHub que se indica a continuación.
A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:
Obtén el código
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.
- Usa componentes de Material Design cuando sea posible para cumplir con los lineamientos de Material Design y permitir una mayor personalización.
- Agrega íconos para dar a los usuarios indicaciones visuales sobre el funcionamiento de las partes de tu app.
- Usa
ConstraintLayout
para posicionar elementos en tu diseño. - Prueba tu app para casos extremos (p. ej., gira tu app en modo de paisaje) y realiza mejoras cuando corresponda.
- Comenta tu código de modo que ayudes a que las personas que lo lean puedan entender tu enfoque.
- Cambia el formato del código y límpialo para que sea lo más conciso posible.
- Como continuación de los codelabs anteriores, actualiza tu app de conversor de unidades de cocina a fin de cumplir más de cerca los lineamientos de Material. Para ello, usa las prácticas recomendadas que aprendiste aquí (como usar los componentes de Material Design).