1. Introducción
Las tarjetas de Wear OS brindan un acceso sencillo a la información y a las acciones que los usuarios necesitan para realizar tareas. Con solo deslizar el dedo sobre la cara de reloj, el usuario puede consultar el pronóstico actualizado o iniciar el cronómetro.
Una tarjeta se ejecuta como parte de la IU del sistema en lugar de ejecutarse en el propio contenedor de la aplicación. Usamos un Servicio para describir el diseño y el contenido de la tarjeta. Luego, la IU del sistema renderizará la tarjeta cuando sea necesario.
Actividades
Crearás una tarjeta para una app de mensajería que muestra conversaciones recientes. Desde esa pantalla, el usuario puede pasar a tres tareas comunes:
- Abrir una conversación
- Buscar una conversación
- Redactar un mensaje nuevo
Qué aprenderás
En este codelab, aprenderás a escribir tu propia tarjeta de Wear OS, incluido lo siguiente:
- Cómo crear un
TileService
- Cómo probar una tarjeta en un dispositivo
- Cómo obtener una vista previa de la IU de una tarjeta en Android Studio
- Cómo desarrollar la IU para una tarjeta
- Cómo agregar imágenes
- Cómo controlar interacciones
Requisitos previos
- Conocimientos básicos sobre Kotlin
2. Cómo prepararte
En este paso, configurarás tu entorno y descargarás un proyecto inicial.
Requisitos
- Actualización de funciones de Android Studio Koala | 2024.1.2 Canary 1 o versiones posteriores
- Emulador o dispositivo Wear OS
Si no estás familiarizado con el uso de Wear OS, lee esta guía rápida antes de comenzar. Se incluyen instrucciones para configurar un emulador de Wear OS y se describe cómo navegar por el sistema.
Cómo descargar el código
Si ya instalaste git, solo ejecuta el siguiente comando para clonar el código de este repositorio.
git clone https://github.com/android/codelab-wear-tiles.git cd codelab-wear-tiles
Si no tienes git, puedes hacer clic en el siguiente botón para descargar todo el código de este codelab:
Abre el proyecto en Android Studio
En la ventana "Welcome to Android Studio", selecciona Open an Existing Project o File > Open y selecciona la carpeta [Ubicación de descarga].
3. Cómo crear una tarjeta básica
El punto de entrada para una tarjeta es el servicio de mosaicos para mapas. En este paso, registrarás un servicio de este tipo y definirás un diseño para la tarjeta.
HelloWorldTileService
Una clase que implementa TileService
debe especificar dos métodos:
onTileResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>
El primer método devuelve un objeto Resources
que asigna IDs de cadenas a los recursos de imagen que usaremos en la tarjeta.
El segundo devuelve la descripción de una tarjeta, incluido su diseño. Aquí es donde definimos el diseño de la tarjeta y cómo se vinculan los datos a ella.
Abre HelloWorldTileService.kt
desde el módulo start
. Todos los cambios que hagas estarán en este módulo. También hay un módulo finished
si quieres ver el resultado de este codelab.
HelloWorldTileService
extiende SuspendingTileService
, un wrapper compatible con corrutinas de Kotlin de la biblioteca de tarjetas de Horologist. Horologist es un grupo de bibliotecas de Google que tiene como objetivo aportar a los desarrolladores de Wear OS un complemento con funciones que comúnmente requieren, pero que aún no están disponibles en Jetpack.
SuspendingTileService
proporciona dos funciones de suspensión, que son versiones de corrutinas equivalentes de las funciones de TileService
:
suspend resourcesRequest(requestParams: ResourcesRequest): Resources
suspend tileRequest(requestParams: TileRequest): Tile
Si deseas obtener más información sobre las corrutinas, consulta la documentación sobre corrutinas de Kotlin en Android.
HelloWorldTileService
aún no está completo. Debemos registrar el servicio en nuestro manifiesto y también proporcionar una implementación para tileLayout
.
Cómo registrar el servicio de tarjetas
Una vez que se registre el servicio de tarjetas en el manifiesto, se mostrará en la lista de tarjetas disponibles que puede agregar el usuario.
Agrega el <service>
dentro del elemento <application>
:
start/src/main/AndroidManifest.xml
<service
android:name="com.example.wear.tiles.hello.HelloWorldTileService"
android:icon="@drawable/ic_waving_hand_24"
android:label="@string/hello_tile_label"
android:description="@string/hello_tile_description"
android:exported="true"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>
<!-- The tile preview shown when configuring tiles on your phone -->
<meta-data
android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/tile_hello" />
</service>
El ícono y la etiqueta se usan (como marcador de posición) cuando la tarjeta se carga por primera vez o si hay un error al momento de cargarla. Los metadatos al final definen una imagen de vista previa que se muestra en el carrusel cuando el usuario agrega una tarjeta.
Cómo definir un diseño para la tarjeta
HelloWorldTileService
tiene una función llamada tileLayout
con un TODO()
como cuerpo. Ahora vamos a reemplazar eso con una implementación en la que definimos el diseño de nuestra tarjeta y vincularemos datos:
start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt
private fun tileLayout(): LayoutElement {
val text = getString(R.string.hello_tile_body)
return LayoutElementBuilders.Box.Builder()
.setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
.setWidth(DimensionBuilders.expand())
.setHeight(DimensionBuilders.expand())
.addContent(
LayoutElementBuilders.Text.Builder()
.setText(text)
.build()
)
.build()
}
Creamos un elemento Text
y lo configuramos dentro de un Box
para poder realizar una alineación básica.
Ya creaste tu primera tarjeta de Wear OS. Instalemos esta tarjeta y veamos cómo se ve.
4. Cómo probar tu tarjeta en un dispositivo
Con el módulo de partida seleccionado en el menú desplegable de la configuración de ejecución, puedes instalar la app (el módulo start
) en tu dispositivo o emulador, y, luego, instalar manualmente la tarjeta, como lo haría un usuario.
En su lugar, usemos Direct Surface Launch, una función que se introdujo en Android Studio Dolphin, para crear una nueva configuración de ejecución y lanzar la tarjeta directamente desde Android Studio. Selecciona "Edit Configurations…" en el menú desplegable del panel superior.
Haz clic en el botón "Add new configuration" y elige "Wear OS Tile". Agrega un nombre descriptivo y, luego, selecciona el módulo Tiles_Code_Lab.start
y la tarjeta HelloWorldTileService
.
Presiona "OK" para terminar.
Direct Surface Launch nos permite probar rápidamente tarjetas en un emulador de Wear OS o en un dispositivo físico. Ejecuta "HelloTile" para probarlo. Debería verse como en la siguiente captura de pantalla.
5. Cómo crear una tarjeta de mensajes
La tarjeta de mensajería que vamos a crear se parece más a una tarjeta del mundo real. A diferencia del ejemplo de HelloWorld, esta carga datos desde un repositorio local, recupera imágenes para mostrar desde la red y controla interacciones para abrir la app, y lo hace directamente desde la tarjeta.
MessagingTileService
MessagingTileService
extiende la clase SuspendingTileService
que vimos antes.
La principal diferencia entre este ejemplo y el anterior es que ahora se observan datos del repositorio y también se obtienen datos de imágenes de la red.
MessagingTileRenderer
MessagingTileRenderer
extiende la clase SingleTileLayoutRenderer
(otra abstracción de las tarjetas de Horologist). Es completamente síncrono: el estado se pasa a las funciones del renderizador, lo que facilita su uso en pruebas y vistas previas de Android Studio.
En el siguiente paso, veremos cómo agregar vistas previas de Android Studio para tarjetas.
6. Cómo agregar funciones de vista previa
Podemos obtener una vista previa de la IU de tarjetas en Android Studio con las funciones de vista previa de tarjetas que se lanzaron en la versión 1.4 de la biblioteca de tarjetas de Jetpack (actualmente en versión alfa). Eso reduce el ciclo de retroalimentación cuando se desarrolla la IU, lo que aumenta la velocidad de desarrollo.
Agrega una vista previa de tarjeta para el elemento MessagingTileRenderer
al final del archivo.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
return TilePreviewData { request ->
MessagingTileRenderer(context).renderTimeline(
MessagingTileState(knownContacts),
request
)
}
}
Ten en cuenta que la anotación @Composable
no se proporciona. Aunque las tarjetas usan la misma IU de vista previa que las Funciones de componibilidad, las tarjetas no usan Compose y no son componibles.
Usa el modo de editor "Pantalla dividida" para obtener una vista previa de la tarjeta:
En el siguiente paso, usaremos Tiles Material para actualizar el diseño.
7. Cómo agregar Tiles Material
Tiles Material proporciona componentes de Material y diseños predefinidos, lo que te permite crear tarjetas que contengan los últimos diseños de materiales para Wear OS.
Agrega la dependencia de Tiles Material a tu archivo build.gradle
:
start/build.gradle
implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"
Agrega el código del botón en la parte inferior del archivo del renderizador y también en la vista previa:
start/src/main/java/MessagingTileRenderer.kt
private fun searchLayout(
context: Context,
clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
.setContentDescription(context.getString(R.string.tile_messaging_search))
.setIconContent(MessagingTileRenderer.ID_IC_SEARCH)
.setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
.build()
También podemos hacer algo similar para crear el diseño de los contactos:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun contactLayout(
context: Context,
contact: Contact,
clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
.setContentDescription(contact.name)
.apply {
if (contact.avatarUrl != null) {
setImageContent(contact.imageResourceId())
} else {
setTextContent(contact.initials)
setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
}
}
.build()
Tiles Material no solo incluye componentes. En lugar de usar una serie de columnas y filas anidadas, podemos usar diseños de Tiles Material para lograr el aspecto deseado rápidamente.
Aquí podemos usar PrimaryLayout
y MultiButtonLayout
para ordenar 4 contactos y el botón de búsqueda. Actualiza la función messagingTileLayout()
en MessagingTileRenderer
con estos diseños:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun messagingTileLayout(
context: Context,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
state: MessagingTileState
) = PrimaryLayout.Builder(deviceParameters)
.setResponsiveContentInsetEnabled(true)
.setContent(
MultiButtonLayout.Builder()
.apply {
// In a PrimaryLayout with a compact chip at the bottom, we can fit 5 buttons.
// We're only taking the first 4 contacts so that we can fit a Search button too.
state.contacts.take(4).forEach { contact ->
addButtonContent(
contactLayout(
context = context,
contact = contact,
clickable = emptyClickable
)
)
}
}
.addButtonContent(searchLayout(context, emptyClickable))
.build()
)
.build()
MultiButtonLayout
admite hasta 7 botones y los distribuirá con el espaciado adecuado para ti.
Agreguemos un CompactChip "nuevo" como el chip "principal" de PrimaryLayout en la función messagingTileLayout()
:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
.setPrimaryChipContent(
CompactChip.Builder(
/* context = */ context,
/* text = */ context.getString(R.string.tile_messaging_create_new),
/* clickable = */ emptyClickable,
/* deviceParameters = */ deviceParameters
)
.setChipColors(ChipColors.primaryChipColors(MessagingTileTheme.colors))
.build()
)
En el siguiente paso, corregiremos las imágenes faltantes.
8. Cómo agregar imágenes
A un alto nivel, las tarjetas constan de dos elementos: elementos de diseño (que hacen referencia a recursos por IDs de cadenas) y los recursos en sí (que pueden ser imágenes).
Hacer que una imagen local esté disponible es una tarea sencilla; si bien no puedes usar los recursos de elementos de diseño de Android directamente, puedes convertirlos de forma trivial al formato requerido con una función conveniente que proporciona Horologist. Luego, usa la función addIdToImageMapping
para asociar la imagen con el identificador de recursos. Por ejemplo:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
addIdToImageMapping(
ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
Para imágenes remotas, usa Coil, un cargador de imágenes basado en corrutinas de Kotlin, para cargar las imágenes a través de la red.
El código ya está escrito para esto:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileService.kt
override suspend fun resourcesRequest(requestParams: ResourcesRequest): Resources {
val avatars = imageLoader.fetchAvatarsFromNetwork(
context = this@MessagingTileService,
requestParams = requestParams,
tileState = latestTileState()
)
return renderer.produceRequestedResources(avatars, requestParams)
}
Como el renderizador de tarjetas es completamente síncrono, el servicio de tarjetas recupera mapas de bits de la red. Al igual que antes, según el tamaño de la imagen, podría resultar más apropiado usar WorkManager para recuperar las imágenes con anticipación. Sin embargo, para este codelab, las recuperamos de forma directa.
Pasamos el mapa de avatars
(de Contact
a Bitmap
) al renderizador como "estado" para los recursos. Ahora, el renderizador puede transformar estos mapas de bits en recursos de imagen para tarjetas.
Este código también está escrito:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
resourceState: Map<Contact, Bitmap>,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
resourceIds: List<String>
) {
addIdToImageMapping(
ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
resourceState.forEach { (contact, bitmap) ->
addIdToImageMapping(
/* id = */ contact.imageResourceId(),
/* image = */ bitmap.toImageResource()
)
}
}
Entonces, si el servicio recupera los mapas de bits y el renderizador los transforma en recursos de imagen, ¿por qué la tarjeta no muestra imágenes?
¡Sí las muestra! Si ejecutas la tarjeta en un dispositivo (con acceso a Internet), deberías ver que las imágenes efectivamente se cargan. El problema se encuentra solo en nuestra vista previa porque no pasamos ningún recurso a TilePreviewData()
.
Para la tarjeta real, recuperaremos mapas de bits de la red y los asignaremos a diferentes contactos, pero no es necesario acceder a la red para las vistas previas ni las pruebas.
Debemos hacer dos cambios. Primero, crea una función previewResources()
que muestre un objeto Resources
:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun previewResources() = Resources.Builder()
.addIdToImageMapping(ID_IC_SEARCH, drawableResToImageResource(R.drawable.ic_search_24))
.addIdToImageMapping(knownContacts[1].imageResourceId(), drawableResToImageResource(R.drawable.ali))
.addIdToImageMapping(knownContacts[2].imageResourceId(), drawableResToImageResource(R.drawable.taylor))
.build()
Luego, actualiza messagingTileLayoutPreview()
para pasar los recursos:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
return TilePreviewData({ previewResources() }) { request ->
MessagingTileRenderer(context).renderTimeline(
MessagingTileState(knownContacts),
request
)
}
}
Ahora, si actualizamos la vista previa, las imágenes deberían mostrar lo siguiente:
En el siguiente paso, controlaremos los clics en cada uno de los elementos.
9. Cómo controlar interacciones
Una de las cosas más útiles que podemos hacer con una tarjeta es proporcionar accesos directos a los recorridos críticos del usuario. Esto es diferente del selector de aplicaciones, que solo abre la app. Aquí tenemos espacio para brindar accesos directos contextuales a una pantalla específica de tu app.
Hasta ahora, usamos emptyClickable
para el chip y cada uno de los botones. Esto está bien para las vistas previas, que no son interactivas, pero veamos cómo agregar acciones para los elementos.
Dos constructores de la clase "ActionBuilders" definen las acciones en las que se puede hacer clic: LoadAction
y LaunchAction
.
LoadAction
Se puede usar LoadAction
si deseas realizar una lógica en el servicio de mosaicos para mapas cuando el usuario hace clic en un elemento, p. ej., incrementando un contador.
.setClickable(
Clickable.Builder()
.setId(ID_CLICK_INCREMENT_COUNTER)
.setOnClick(ActionBuilders.LoadAction.Builder().build())
.build()
)
)
Cuando hagas clic en esta opción, se llamará a onTileRequest
en tu servicio (tileRequest
en SuspendingTileService
), por lo que es una buena oportunidad para actualizar la IU de la tarjeta:
override suspend fun tileRequest(requestParams: TileRequest): Tile {
if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
// increment counter
}
// return an updated tile
}
LaunchAction
Se puede usar LaunchAction
para iniciar una actividad. En MessagingTileRenderer
, actualicemos el botón de búsqueda en el que se puede hacer clic.
El botón de búsqueda se define con la función searchLayout()
en MessagingTileRenderer
. Ya toma un Clickable
como parámetro, pero hasta ahora hemos pasado emptyClickable
, una implementación no-op que no hace nada cuando se hace clic en el botón.
Actualicemos messagingTileLayout()
para que pase una acción de clic real.
- Agrega un parámetro nuevo,
searchButtonClickable
(de tipoModifiersBuilders.Clickable
). - Pasa esto a la función
searchLayout()
existente.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun messagingTileLayout(
context: Context,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
state: MessagingTileState,
searchButtonClickable: ModifiersBuilders.Clickable
...
.addButtonContent(searchLayout(context, searchButtonClickable))
También necesitamos actualizar renderTile
, que es donde llamamos a messagingTileLayout
, ya que agregamos un parámetro nuevo (searchButtonClickable
). Usaremos la función launchActivityClickable()
para crear un nuevo elemento en el que se pueda hacer clic y pasar openSearch()
ActionBuilder
como acción:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
override fun renderTile(
state: MessagingTileState,
deviceParameters: DeviceParametersBuilders.DeviceParameters
): LayoutElementBuilders.LayoutElement {
return messagingTileLayout(
context = context,
deviceParameters = deviceParameters,
state = state,
searchButtonClickable = launchActivityClickable("search_button", openSearch())
)
}
Abre launchActivityClickable
para ver cómo funcionan estas funciones (ya definidas):
start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt
internal fun launchActivityClickable(
clickableId: String,
androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
.setId(clickableId)
.setOnClick(
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(androidActivity)
.build()
)
.build()
Es muy similar a LoadAction
. La diferencia principal es que llamamos a setAndroidActivity
. En el mismo archivo, tenemos varios ejemplos de ActionBuilder.AndroidActivity
.
En el caso de openSearch
, que usamos para este elemento en el que se puede hacer clic, llamamos a setMessagingActivity
y pasamos una cadena adicional para identificar con qué botón se hizo clic.
start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt
internal fun openSearch() = ActionBuilders.AndroidActivity.Builder()
.setMessagingActivity()
.addKeyToExtraMapping(
MainActivity.EXTRA_JOURNEY,
ActionBuilders.stringExtra(MainActivity.EXTRA_JOURNEY_SEARCH)
)
.build()
...
internal fun ActionBuilders.AndroidActivity.Builder.setMessagingActivity(): ActionBuilders.AndroidActivity.Builder {
return setPackageName("com.example.wear.tiles")
.setClassName("com.example.wear.tiles.messaging.MainActivity")
}
Ejecuta la tarjeta (asegúrate de ejecutar la tarjeta "mensajería", no la tarjeta "hola") y haz clic en el botón de búsqueda. Debería abrirse la MainActivity
y mostrar texto para confirmar que se hizo clic en el botón de búsqueda.
Agregar acciones para los demás botones es similar. ClickableActions
contiene las funciones que necesitas. Si necesitas una pista, consulta MessagingTileRenderer
en el módulo finished
.
10. Felicitaciones
¡Felicitaciones! Ya aprendiste a crear una tarjeta para Wear OS.
¿Qué sigue?
Para obtener más información, consulta las implementaciones de tarjetas doradas en GitHub, la guía de tarjetas de Wear OS y los lineamientos de diseño.