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 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
- Mise à jour groupée d'Android Studio Koala | 02/01/2024 Canary 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.
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 :
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 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.
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 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 :
À 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()
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()
)
À 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 :
À 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.
- Ajoutez un paramètre
searchButtonClickable
(de typeModifiersBuilders.Clickable
). - 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.