Créer votre première carte dans Wear OS

1. Introduction

montre animée, l'utilisateur balaie le cadran jusqu'à la première carte qui représente une prévision, puis jusqu'à la carte d'un minuteur, et revient en arrière

Les cartes Wear OS permettent d'accéder facilement aux informations et aux actions dont les utilisateurs ont besoin pour effectuer une tâche. Il suffit de balayer l'écran à partir du cadran pour consulter les dernières prévisions ou lancer un minuteur.

Une carte s'exécute dans l'interface utilisateur du système au lieu de s'exécuter dans son propre conteneur d'application. Nous utilisons un service pour décrire la mise en page et le contenu de cette carte. L'UI du système affiche ensuite la carte si nécessaire.

Objectifs de l'atelier

35a459b77a2c9d52.png

Vous créerez une carte qui affichera les conversations récentes pour une application de chat. À partir de cette surface, l'utilisateur peut effectuer trois tâches courantes :

  • Ouvrir une conversation
  • Rechercher une conversation
  • Écrire un nouveau message

Points abordés

Dans cet atelier de programmation, vous apprendrez à écrire votre propre carte Wear OS, ce qui comprend les tâches suivantes :

  • Créer un élément TileService
  • Tester une carte sur un appareil
  • Prévisualiser l'interface utilisateur d'une carte dans Android Studio
  • Développer l'interface utilisateur d'une carte
  • Ajouter des images
  • Gérer les interactions

Conditions préalables

  • Connaissances de base du langage Kotlin

2. Configuration

Au cours de cette étape, vous configurerez votre environnement et téléchargerez le projet de démarrage.

Ce dont vous avez besoin

Si vous ne savez pas comment utiliser Wear OS, nous vous recommandons de lire ce guide rapide avant de commencer. Il contient des instructions pour configurer un émulateur Wear OS et décrit comment naviguer dans le système.

Télécharger le code

Si git est installé, vous pouvez simplement exécuter la commande ci-dessous pour cloner le code à partir de ce dépôt.

git clone https://github.com/android/codelab-wear-tiles.git
cd codelab-wear-tiles

Si vous n'avez pas git, cliquez sur le bouton ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :

Télécharger le fichier ZIP

Ouvrir un projet dans Android Studio

Dans la fenêtre "Bienvenue dans Android Studio", sélectionnez c01826594f360d94.png Ouvrir un projet existant ouFichier > Ouvrir, puis sélectionnez le dossier [Emplacement de téléchargement].

3. Créer une carte de base

Le point d'entrée d'une carte est le service de cartes. Au cours de cette étape, vous allez enregistrer un service de cartes et définir une mise en page de carte.

HelloWorldTileService

Une classe qui implémente TileService doit spécifier deux méthodes :

  • onTileResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
  • onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>

La première méthode renvoie un objet Resources qui mappe les ID de chaîne aux ressources image que nous utiliserons dans notre carte.

La seconde renvoie la description d'une carte, y compris sa mise en page. C'est là que nous définirons la mise en page d'une carte et la manière dont les données y sont liées.

Ouvrez HelloWorldTileService.kt à partir du module start. Toutes les modifications que vous allez apporter seront incluses dans ce module. Si vous voulez voir le résultat de cet atelier de programmation, il existe également un module finished.

HelloWorldTileService complète SuspendingTileService, un wrapper compatible avec la coroutine Kotlin issu de la bibliothèque Horologist Tiles. Horologist est un groupe de bibliothèques Google visant à fournir aux développeurs Wear OS des fonctionnalités complémentaires qui sont couramment plébiscitées, mais qui ne sont pas encore disponibles dans Jetpack.

SuspendingTileService fournit deux fonctions de suspension, qui sont des équivalents de coroutine des fonctions de TileService :

  • suspend resourcesRequest(requestParams: ResourcesRequest): Resources
  • suspend tileRequest(requestParams: TileRequest): Tile

Pour en savoir plus sur les coroutines, consultez la documentation sur les coroutines Kotlin sur Android.

HelloWorldTileService n'est pas encore terminé. Nous devons enregistrer le service dans notre fichier manifeste et fournir également une implémentation pour tileLayout.

Enregistrer le service de cartes

Une fois le service de cartes enregistré dans le fichier manifeste, il apparaît dans la liste des cartes que l'utilisateur peut ajouter.

Ajoutez <service> dans l'élément <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>

L'icône et le libellé sont utilisés (comme des espaces réservés) lors du chargement initial de la carte ou en cas d'erreur lors du chargement. Les métadonnées à la fin définissent une image d'aperçu qui s'affiche dans le carrousel lorsque l'utilisateur ajoute une carte.

Définir la mise en page de la carte

HelloWorldTileService possède une fonction appelée tileLayout avec l'élément TODO() comme corps. Nous allons maintenant remplacer cela par une implémentation dans laquelle nous définirons la mise en page de la carte et associerons des données :

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()
}

Nous créons un élément Text et le définissons dans un élément Box afin de pouvoir effectuer un alignement de base.

Et voilà notre première carte Wear OS créée. Installons cette carte et voyons ce à quoi elle ressemble.

4. Tester votre carte sur un appareil

Une fois le module de démarrage sélectionné dans le menu déroulant de la configuration d'exécution, vous pouvez installer l'application (module start) sur votre appareil ou dans un émulateur, puis installer manuellement la carte, comme le ferait un utilisateur.

À la place, nous allons utiliser Direct Surface Launch, une fonctionnalité ajoutée avec Android Studio Dolphin, qui permet de créer une configuration d'exécution afin de lancer la carte directement depuis Android Studio. Sélectionnez "Modifier les configurations" dans le menu déroulant du panneau supérieur.

Menu déroulant de modification de la configuration dans le panneau supérieur d'Android Studio. L'option "Modifier les configurations" est mise en surbrillance.

Cliquez sur le bouton "Ajouter une configuration", puis sélectionnez "Carte Wear OS". Ajoutez un nom descriptif, puis sélectionnez le module Tiles_Code_Lab.start et la carte HelloWorldTileService.

Appuyez sur "OK" pour terminer.

Menu de modification de la configuration avec une carte Wear OS appelée HelloTile en cours de configuration.

Direct Surface Launch permet de tester rapidement les cartes dans un émulateur Wear OS ou sur un appareil physique. Testez-le par vous-même en exécutant "HelloTile". Une capture d'écran semblable à celle ci-dessous devrait s'afficher.

Montre ronde indiquant que l'heure est venue de créer une carte en texte blanc sur fond noir

5. Créer une carte de messages

Montre ronde avec cinq boutons ronds disposés en pyramide de type 2x3. Les 1er et 3e boutons affichent des initiales en violet, les 2e et 4e images affichent des photos de profil, et le dernier bouton est une icône de recherche. Sous les boutons, un chip compact violet affiche le texte "New" (Nouveau) en noir.

La carte de messages que nous allons créer s'apparente davantage à une carte réelle. Contrairement à l'exemple HelloWorld, celui-ci charge les données à partir d'un dépôt local, extrait les images à afficher à partir du réseau et gère les interactions pour ouvrir l'application, directement à partir de la carte.

MessagingTileService

MessagingTileService complète la classe SuspendingTileService que nous avons vue précédemment.

La principale différence entre cet exemple et l'exemple précédent est que nous observons désormais des données provenant du dépôt et que nous récupérons également les données image à partir du réseau.

MessagingTileRenderer

MessagingTileRenderer complète la classe SingleTileLayoutRenderer (autre abstraction de la bibliothèque Horologist Tiles). Il est totalement synchrone : l'état est transmis aux fonctions du moteur de rendu, ce qui facilite l'utilisation dans les tests et les aperçus Android Studio.

À l'étape suivante, nous verrons comment ajouter des aperçus Android Studio pour des cartes.

6. Ajouter des fonctions d'aperçu

Nous pouvons prévisualiser l'UI des cartes dans Android Studio à l'aide des fonctions d'aperçu des cartes publiées dans la version 1.4 de la bibliothèque Tiles de Jetpack (actuellement en version alpha). Cela permet de raccourcir la boucle de rétroaction lors du développement de l'interface utilisateur, ce qui augmente la vitesse de développement.

Ajoutez un aperçu de carte pour le MessagingTileRenderer à la fin du fichier.

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
        )
    }
}

Notez que l'annotation @Composable n'est pas fournie. Bien que les cartes utilisent la même UI d'aperçu que les fonctions composables, elles n'utilisent pas Compose et ne sont pas composables.

Utilisez la vue d'écran partagé pour afficher un aperçu de la carte :

Vue d'écran partagé d'Android Studio avec aperçu du code à gauche et image de la carte à droite.

À l'étape suivante, nous utiliserons Tiles Material pour mettre à jour la mise en page.

7. Ajouter Tiles Material

Tiles Material propose des composants Material et des mises en page prédéfinis qui vous permettent de créer des cartes utilisant les dernières consignes Material Design pour Wear OS.

Ajoutez la dépendance Tiles Material au fichier build.gradle :

start/build.gradle

implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"

Ajoutez le code du bouton en bas du fichier de moteur de rendu, ainsi que l'aperçu :

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()

Nous pouvons procéder de la même manière pour créer la mise en page des contacts :

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 ne se limite pas aux composants. Au lieu d'utiliser une série de colonnes et de lignes imbriquées, nous pouvons utiliser les mises en page de Tiles Material pour obtenir rapidement l'aspect souhaité.

Ici, nous pouvons utiliser PrimaryLayout et MultiButtonLayout pour organiser quatre contacts et le bouton de recherche. Mettez à jour la fonction messagingTileLayout() dans MessagingTileRenderer avec les mises en page suivantes :

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()

96fee80361af2c0f.png

MultiButtonLayout accepte jusqu'à sept boutons et les dispose automatiquement en les espaçant de manière appropriée.

Ajoutons un "nouveau" CompactChip en tant que chip "primaire" de PrimaryLayout dans la fonction 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()
    )

2041bdca8a46458b.png

À l'étape suivante, nous allons résoudre le problème d'images manquantes.

8. Ajouter des images

De manière générale, les cartes se composent de deux éléments : des éléments de mise en page (qui référence des ressources par des ID de chaîne) et les ressources elles-mêmes (qui peuvent être des images).

Rendre une image locale disponible est une tâche simple : même si vous ne pouvez pas utiliser directement les ressources drawable Android, vous pouvez facilement les convertir au format requis à l'aide d'une fonction pratique fournie par Horologist. Ensuite, utilisez la fonction addIdToImageMapping pour associer l'image à l'identifiant de ressource. Exemple :

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

addIdToImageMapping(
    ID_IC_SEARCH,
    drawableResToImageResource(R.drawable.ic_search_24)
)

Pour les images distantes, utilisez Coil, un chargeur d'images basé sur les coroutines Kotlin, pour les charger sur le réseau.

Le code requis est déjà écrit :

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)
}

Comme le moteur de rendu des cartes est entièrement synchrone, le service de cartes récupère les bitmaps à partir du réseau. Comme mentionné précédemment, en fonction de la taille de l'image, il peut être plus approprié d'utiliser WorkManager afin de récupérer les images à l'avance. Toutefois, pour cet atelier de programmation, nous les récupérerons directement.

Nous transmettons la map avatars (Contact à Bitmap) au moteur de rendu en tant qu'"état" pour les ressources. Le moteur de rendu peut désormais convertir ces bitmaps en ressources image pour les cartes.

Ce code est déjà écrit :

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()
        )
    }
}

Si le service récupère les bitmaps et que le moteur de rendu les convertit en ressources image, pourquoi la carte n'affiche-t-elle pas les images ?

Les images s'affichent. Si vous exécutez la carte sur un appareil (avec accès à Internet), les images devraient se charger. Le problème se situe uniquement dans notre aperçu, car nous n'avons transmis aucune ressource à TilePreviewData().

Dans la carte réelle, nous récupérons les bitmaps à partir du réseau et les mappons avec les différents contacts. Toutefois, pour les aperçus et les tests, nous n'avons pas du tout besoin d'appeler le réseau.

Nous devons apporter deux modifications. Commencez par créer une fonction previewResources() qui renvoie un objet 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()

Ensuite, modifiez messagingTileLayoutPreview() pour transmettre les ressources :

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
        )
    }
}

À présent, si nous actualisons l'aperçu, les images devraient s'afficher :

3142b42717407059.png

À l'étape suivante, nous traiterons les clics sur chacun des éléments.

9. Gérer les interactions

Les raccourcis vers les parcours utilisateur les plus importants sont l'un des principaux atouts d'une carte. À ne pas confondre avec le lanceur d'applications, qui ouvre simplement l'application ! Dans le cas présent, nous avons l'espace nécessaire pour fournir des raccourcis contextuels sur un écran spécifique de votre application.

Jusqu'à présent, nous avons utilisé emptyClickable pour le chip et chacun des boutons. Cette approche convient pour les aperçus, qui ne sont pas interactifs, mais voyons comment ajouter des actions correspondant aux éléments.

Deux compilateurs de la classe "ActionBuilders" définissent des actions cliquables : LoadAction et LaunchAction.

LoadAction

Un élément LoadAction peut être utilisé si vous souhaitez exécuter la logique dans le service de cartes lorsque l'utilisateur clique sur un élément, par exemple en incrémentant un compteur.

.setClickable(
    Clickable.Builder()
        .setId(ID_CLICK_INCREMENT_COUNTER)
        .setOnClick(ActionBuilders.LoadAction.Builder().build())
        .build()
    )
)

Lorsque vous cliquez dessus, onTileRequest est appelé dans votre service (tileRequest dans SuspendingTileService). Il s'agit donc d'une bonne occasion d'actualiser l'interface utilisateur de la carte :

override suspend fun tileRequest(requestParams: TileRequest): Tile {
    if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
        // increment counter
    }
    // return an updated tile
}

LaunchAction

L'élément LaunchAction peut être utilisé pour lancer une activité. Dans MessagingTileRenderer, mettons à jour le bouton cliquable pour la recherche.

Le bouton de recherche est défini par la fonction searchLayout() dans MessagingTileRenderer. Un élément Clickable est déjà utilisé comme paramètre, mais jusqu'à présent, nous avons transmis emptyClickable, une implémentation non opérationnelle qui ne fait rien lorsque l'utilisateur clique sur le bouton.

Mettons à jour messagingTileLayout() afin qu'il transmette une action de clic réelle.

  1. Ajoutez un paramètre searchButtonClickable (de type ModifiersBuilders.Clickable).
  2. Transmettez-le à la fonction searchLayout() existante.

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))

Nous devons également mettre à jour renderTile, où nous appelons messagingTileLayout, car nous venons d'ajouter un nouveau paramètre (searchButtonClickable). Nous utiliserons la fonction launchActivityClickable() pour créer un élément cliquable, en transmettant l'ActionBuilder openSearch() comme action :

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())
    )
}

Ouvrez launchActivityClickable pour voir ces fonctions (déjà définies) à l'œuvre :

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()

Cet élément est très semblable à LoadAction, la principale différence étant que nous appelons setAndroidActivity. Le même fichier contient plusieurs exemples ActionBuilder.AndroidActivity.

Pour openSearch, que nous utilisons pour cet élément cliquable, nous appelons setMessagingActivity et nous transmettons un extra de type "chaîne" afin d'identifier quel clic a eu lieu sur le bouton.

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")
}

Exécutez la carte (assurez-vous d'exécuter la carte "Messaging", et non la carte "Hello"), puis cliquez sur le bouton de recherche. Il devrait ouvrir MainActivity et afficher du texte pour confirmer que l'utilisateur a cliqué sur le bouton de recherche.

L'ajout d'actions pour les autres éléments est similaire. ClickableActions contient les fonctions dont vous avez besoin. Si vous avez besoin d'aide, consultez la section MessagingTileRenderer du module finished.

10. Félicitations

Félicitations ! Vous avez appris à créer une carte pour Wear OS.

Et maintenant ?

Pour en savoir plus, consultez les implémentations de Golden Tiles sur GitHub, le guide des cartes Wear OS et les consignes de conception.