1. Introduction
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
Vous créerez une carte qui affichera les conversations récentes pour une application de messagerie. À partir de cette surface, l'utilisateur pourra effectuer directement l'une des trois tâches courantes suivantes :
- 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
- Android Studio Dolphin (2021.3.1) ou version ultérieure
- Appareil ou émulateur Wear OS
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. Pour vérifier s'il est installé, saisissez git –version dans le terminal ou la ligne de commande, et assurez-vous qu'il fonctionne correctement.
git clone https://github.com/googlecodelabs/wear-tiles.git cd 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 :
Ouvrir un projet dans Android Studio
Dans la fenêtre "Bienvenue dans Android Studio", sélectionnez 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 fonctions :
onResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>
La première mappe les ID de chaîne par rapport à une ressource image. C'est là que les ressources image que nous utiliserons dans la carte sont fournies.
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 CoroutinesTileService
, 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.
CoroutinesTileService
fournit deux fonctions de suspension, qui sont des versions 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
Il est nécessaire d'enregistrer le service de cartes dans le fichier manifeste pour que le système le reconnaisse. Une fois enregistré, 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.
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.
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.
5. Créer une carte de messages
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 CoroutinesTileService
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.
Pour toute tâche potentiellement longue (appels réseau, par exemple), il serait plus approprié d'utiliser un outil comme WorkManager, car les fonctions des services de carte ont des délais d'attente relativement courts. Cet atelier de programmation ne couvre pas l'utilisation de WorkManager. Pour en savoir plus, consultez cet atelier de programmation.
MessagingTileRenderer
MessagingTileRenderer
complète la classe TileRenderer
(autre abstraction de la bibliothèque Horologist Tiles). Il est totalement synchrone. Autrement dit, 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
Vous pouvez afficher un aperçu de l'interface utilisateur d'une carte dans Android Studio avec TileLayoutPreview
(et similaire) à partir de la bibliothèque Horologist Tiles. Cette approche permet de raccourcir la boucle de rétroaction lors du développement de l'interface utilisateur, ce qui accélère considérablement l'itération.
Nous utiliserons les outils de Jetpack Compose pour afficher cet aperçu. C'est pourquoi l'annotation @Composable
s'affiche sur la fonction d'aperçu ci-dessous. Vous pouvez vous familiariser davantage avec les aperçus modulables si vous le souhaitez. Toutefois, vous n'avez pas besoin de suivre cet atelier de programmation.
Ajoutez un aperçu modulable pour MessagingTileRenderer
à la fin du fichier.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
TileLayoutPreview(
state = MessagingTileState(MessagingRepo.knownContacts),
resourceState = emptyMap(),
renderer = MessagingTileRenderer(LocalContext.current)
)
}
Notez que la fonction modulable utilise TileLayoutPreview
. Il n'est pas possible d'afficher directement un aperçu de la mise en page des cartes.
Utilisez la vue d'écran partagé pour afficher un aperçu de la carte :
Nous transmettons des données artificielles dans MessagingTileState
et nous n'avons pas (encore) d'état de ressource. Nous pouvons donc transmettre une map vide.
À l'étape suivante, nous utiliserons Tiles Material pour mettre à jour la mise en page.
7. Ajouter Tiles Material
Tiles Material propose des composants 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.tiles:tiles-material:$tilesVersion"
Selon la complexité de votre conception, il peut être utile de localiser au même endroit le code de mise en page avec le moteur de rendu, en utilisant des fonctions de niveau supérieur dans le même fichier pour encapsuler une unité logique de l'interface utilisateur.
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()
@IconSizePreview
@Composable
private fun SearchButtonPreview() {
LayoutElementPreview(
searchLayout(
context = LocalContext.current,
clickable = emptyClickable
)
) {
addIdToImageMapping(
MessagingTileRenderer.ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
}
}
LayoutElementPreview
est semblable à TileLayoutPreview
, mais est destiné à des composants individuels tels qu'un bouton, un chip ou un libellé. Le lambda final tout à la fin nous permet de spécifier le mappage de l'ID de ressource (avec les ressources image). Ici, nous mappons ID_IC_SEARCH
avec la ressource image illustrant un bouton de recherche.
La vue d'écran partagé offre un aperçu du bouton de recherche :
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)
.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
accepte jusqu'à sept boutons et les dispose automatiquement en les espaçant de manière appropriée. Ajoutons également le chip "New" (Nouveau) dans PrimaryLayout
, au niveau du compilateur 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()
)
À l'étape suivante, nous allons résoudre le problème d'images manquantes.
8. Ajouter des images
L'affichage d'une image locale sur une carte est une tâche simple : il suffit de créer un mappage avec l'image à partir d'un ID de chaîne (que vous utilisez dans la mise en page), en utilisant la fonction utile Horologist Tiles pour charger le drawable et le convertir en ressource image. Un exemple est disponible dans SearchButtonPreview
:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
addIdToImageMapping(
ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
Pour la carte de messages, nous devons également charger des images depuis le réseau (et pas uniquement des ressources locales). Pour cela, nous utiliserons Coil, un chargeur d'images basé sur les coroutines Kotlin.
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)
}
Étant donné que le moteur de rendu des cartes est entièrement synchrone, le service de cartes extrait 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: MutableList<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 transmettons toujours un élément emptyMap()
pour resourceState
.
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.
Mettez à jour MessagingTileRendererPreview()
afin de fournir des bitmaps aux deux contacts qui en ont besoin :
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
val state = MessagingTileState(MessagingRepo.knownContacts)
val context = LocalContext.current
TileLayoutPreview(
state = state,
resourceState = mapOf(
state.contacts[1] to (context.getDrawable(R.drawable.ali) as BitmapDrawable).bitmap,
state.contacts[2] to (context.getDrawable(R.drawable.taylor) as BitmapDrawable).bitmap,
),
renderer = MessagingTileRenderer(context)
)
}
À présent, si nous actualisons l'aperçu, les images devraient s'afficher :
À 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 CoroutinesTileService
). 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. Ajoutez le paramètre searchButtonClickable
et transmettez-le à searchLayout()
:
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, 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 Golden Tiles sur GitHub et le guide des cartes Wear OS.