1. Avant de commencer
Conditions préalables
- Vous maîtrisez la création d'applis Android.
- Vous maîtrisez Jetpack Compose.
Ce dont vous avez besoin
- La dernière version stable d'Android Studio
Points abordés
- Principes de base des mises en page adaptatives et de Navigation 3
- Implémenter le glisser-déposer
- Prise en charge des raccourcis clavier
- Activer les menus contextuels
2. Configuration
Pour commencer, procédez comme suit :
- Lancez Android Studio.
- Cliquez sur File > New >
Project from Version control
(Fichier > Nouveau >Project from Version control
). - Collez l'URL :
https://github.com/android/socialite.git
- Cliquez sur
Clone
.
Attendez que le projet se charge complètement.
- Ouvrez le terminal et exécutez :
$ git checkout codelab-adaptive-apps-start
- Exécuter une synchronisation Gradle
Dans Android Studio, sélectionnez File > Sync Project with Gradle Files (Fichier > Synchroniser le projet avec les fichiers Gradle).
- (Facultatif) Télécharger l'émulateur pour les grands ordinateurs
Dans Android Studio, sélectionnez Tools > Device Manager > + > Create Virtual Device > New hardware profile (Outils > Gestionnaire d'appareils > + > Créer un appareil virtuel > Nouveau profil matériel).
Sélectionnez le type d'appareil : Ordinateur.
Taille de l'écran : 14 pouces
Résolution : 1920 x 1080 px
Cliquez sur Finish (Terminer).
- Exécuter l'application sur une tablette ou un émulateur d'ordinateur de bureau
3. Comprendre l'application exemple
Dans ce tutoriel, vous travaillerez avec un exemple d'application de chat appelée Socialite, créée avec Jetpack Compose.
Dans cette application, vous pouvez discuter avec différents animaux, qui répondent à vos messages chacun à leur manière.
Pour le moment, il s'agit d'une application destinée aux appareils mobiles qui n'est pas optimisée pour les grands appareils tels que les tablettes ou les ordinateurs.
Nous allons adapter l'application aux grands écrans et ajouter quelques fonctionnalités pour améliorer l'expérience sur tous les facteurs de forme.
Voyons cela plus en détail.
4. Mises en page adaptatives et principes de base de Navigation 3
$ git checkout codelab-adaptive-apps-step-1
Actuellement, l'application n'affiche qu'un seul volet à la fois, quelle que soit la surface d'écran disponible.
Pour résoudre ce problème, nous allons utiliser adaptive layouts
, qui affiche un ou plusieurs volets en fonction de la taille de la fenêtre actuelle. Dans cet atelier de programmation, nous allons utiliser des mises en page adaptatives pour afficher automatiquement les écrans chat list
et chat detail
côte à côte, lorsqu'il y a suffisamment d'espace sur la fenêtre.
Les mises en page adaptatives sont conçues pour s'intégrer parfaitement à n'importe quelle application.
Dans ce tutoriel, nous allons nous concentrer sur leur utilisation avec la bibliothèque Navigation 3, sur laquelle l'application Socialite est compilée.
Principes de base de Navigation 3
Pour comprendre Navigation 3, commençons par expliquer quelques termes :
- NavEntry : contenu affiché dans une application vers laquelle un utilisateur peut naviguer. Il est identifié de manière unique par une clé. Un élément NavEntry n'a pas besoin de remplir toute la fenêtre disponible pour l'application. Plusieurs NavEntry peuvent être affichés en même temps (nous y reviendrons plus tard).
- Key : identifiant unique d'un NavEntry. Les clés sont stockées dans la pile "Retour".
- Pile "Retour" : pile de clés représentant des éléments NavEntry qui ont déjà été affichés ou qui sont actuellement affichés. Pour naviguer, ajoutez des clés à la pile ou supprimez-en de la pile.
Dans Socialite, le premier écran que nous souhaitons afficher lorsque l'utilisateur lance l'application est la liste de chat. Nous allons donc créer la pile "Retour" et l'initialiser avec la clé représentant cet écran.
Main.kt
// Create a new back stack
val backStack = rememberNavBackStack(ChatsList)
...
// Navigate to a particular chat
backStack.add(ChatThread(chatId = chatId))
...
// Navigate back
backStack.removeLastOrNull()
Implémentation de Navigation 3
Nous allons implémenter Navigation 3 directement dans le composable du point d'entrée Main
.
Annulez la mise en commentaire de l'appel de fonction MainNavigation
pour associer la logique de navigation.
Commençons maintenant à créer l'infrastructure de navigation.
Tout d'abord, créez la pile "Retour". Il s'agit de la pierre angulaire de Navigation 3.
NavDisplay
Jusqu'à présent, nous avons abordé plusieurs concepts de Navigation 3. Mais comment la bibliothèque détermine-t-elle l'objet qui représente la pile "Retour" et la façon dont ses éléments sont transformés en UI ?
Découvrez NavDisplay
. C'est le composant qui rassemble tout et affiche la pile "Retour". Il nécessite quelques paramètres importants. Passons-les en revue un par un.
Paramètre 1 : pile "Retour"
NavDisplay
a besoin d'accéder à la pile "Retour" pour afficher son contenu. Transmettons-le.
Paramètre 2 : EntryProvider
EntryProvider
est un lambda qui transforme les clés de la pile "Retour" en contenu d'UI composable. Il utilise une clé et renvoie un NavEntry
, qui contient le contenu à afficher, ainsi que des métadonnées sur la manière de l'afficher (nous y reviendrons plus tard).
NavDisplay
appelle ce lambda chaque fois qu'il doit obtenir du contenu pour une clé donnée, par exemple lorsqu'une nouvelle clé est ajoutée à la pile "Retour".
Actuellement, si vous cliquez sur l'icône Timeline dans Socialite, le message "Unknown back stack key: Timeline" (Clé de pile "Retour" inconnue : Timeline) s'affiche.
En effet, même si la clé Timeline est ajoutée à la pile "Retour", EntryProvider
ne sait pas comment l'afficher et utilise donc l'implémentation par défaut. La même chose se produit lorsque nous cliquons sur l'icône Settings (Paramètres). Pour résoudre ce problème, nous allons nous assurer que EntryProvider
gère correctement les clés de la pile "Retour" pour Timeline et Settings.
Paramètre 3 : SceneStrategy
Le prochain paramètre important de NavDisplay
est SceneStrategy
. Ce paramètre est utilisé lorsque nous souhaitons afficher plusieurs éléments NavEntry
en même temps. Chaque stratégie définit la manière dont plusieurs éléments NavEntry
sont affichés côte à côte ou superposés.
Par exemple, si nous utilisons l'élément DialogSceneStrategy
et que nous marquons certains NavEntry
avec des métadonnées spéciales, il s'affichera sous la forme d'une boîte de dialogue au-dessus du contenu actuel au lieu de s'afficher en plein écran.
Dans notre cas, nous allons utiliser un autre élément SceneStrategy : ListDetailSceneStrategy
. Il est conçu pour la mise en page standard liste/détails.
Commençons par l'ajouter dans le constructeur NavDisplay
.
sceneStrategy = rememberListDetailSceneStrategy(),
Nous devons maintenant marquer le NavEntry
de ChatList
comme volet de liste et le NavEntry de ChatThread
comme volet de détails. La stratégie pourra ainsi déterminer quand ces deux éléments NavEntry se trouvent dans la pile "Retour" et doivent être affichés côte à côte.
À l'étape suivante, marquez NavEntry
de ChatsList
comme volet de liste.
entryProvider = { backStackKey ->
when (backStackKey) {
is ChatsList -> NavEntry(
key = backStackKey,
metadata = ListDetailSceneStrategy.listPane(),
) {
...
}
...
}
}
De même, marquez NavEntry
de ChatThread
comme volet de détails.
entryProvider = { backStackKey ->
when (backStackKey) {
is ChatThread -> NavEntry(
key = backStackKey,
metadata = ListDetailSceneStrategy.detailPane(),
) {
...
}
...
}
}
Nous avons ainsi intégré des mises en page adaptatives à notre application.
5. Glisser-déposer
$ git checkout codelab-adaptive-apps-step-2
Au cours de cette étape, nous allons ajouter la prise en charge du glisser-déposer, ce qui permettra aux utilisateurs de faire glisser des images depuis l'application Files vers Socialite.
Notre objectif est d'activer le glisser-déposer dans la zone message list
, définie par le composable MessageList
situé dans le fichier ChatScreen.kt
.
Dans Jetpack Compose, la prise en charge du glisser-déposer est implémentée par le modificateur dragAndDropTarget
. Nous l'appliquons aux composables qui doivent accepter les éléments déposés.
Modifier.dragAndDropTarget(
shouldStartDragAndDrop = { event ->
// condition to accept dragged item
},
target = // DragAndDropTarget
)
Le modificateur comporte deux paramètres.
- Le premier,
shouldStartDragAndDrop
, permet au composable de filtrer les événements de glisser-déposer. Dans notre cas, nous ne voulons accepter que les images et ignorer tous les autres types de données. - Le second,
target
, est un rappel qui définit la logique de gestion des événements de glisser-déposer acceptés.
Commençons par ajouter dragAndDropTarget
au composable MessageList
.
.dragAndDropTarget(
shouldStartDragAndDrop = { event ->
event.mimeTypes().any { it.startsWith("image/") }
},
target = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
TODO("Not yet implemented")
}
}
}
),
L'objet de rappel target
doit implémenter la méthode onDrop()
, qui utilise un DragAndDropEvent
comme argument.
Cette méthode est appelée lorsque l'utilisateur dépose un élément sur le composable. Elle renvoie true
si l'élément a été traité, et false
s'il a été refusé.
Chaque DragAndDropEvent
contient un objet ClipData
, qui encapsule les données qui sont en train d'être déplacées.
Les données contenues dans ClipData
sont un tableau d'objets Item
. Étant donné que plusieurs éléments peuvent être déplacés en même temps, chaque Item
représente l'un d'entre eux.
target = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
val clipData = event.toAndroidDragEvent().clipData
if (clipData != null && clipData.itemCount > 0) {
repeat(clipData.itemCount) { i ->
val item = clipData.getItemAt(i)
// TODO: Implement Item handling
}
return true
}
return false
}
}
}
Un Item
peut contenir des données sous la forme d'un URI, de texte ou d'Intent
.
Dans notre cas, comme nous n'acceptons que les images, nous recherchons spécifiquement un URI.
Si un Item
en contient un, nous devons :
- Demander l'autorisation de glisser-déposer pour accéder à l'URI
- Gérer l'URI (dans notre cas, en appelant la fonction
onMediaItemAttached()
déjà implémentée) - Libérer l'autorisation
override fun onDrop(event: DragAndDropEvent): Boolean {
val clipData = event.toAndroidDragEvent().clipData
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& clipData != null && clipData.itemCount > 0) {
repeat(clipData.itemCount) { i ->
val item = clipData.getItemAt(i)
val passedUri = item.uri?.toString()
if (!passedUri.isNullOrEmpty()) {
val dropPermission = activity
.requestDragAndDropPermissions(
event.toAndroidDragEvent()
)
try {
val mimeType = context.contentResolver
.getType(passedUri.toUri()) ?: ""
onMediaItemAttached(MediaItem(passedUri, mimeType))
} finally {
dropPermission.release()
}
}
}
return true
}
return false
}
À ce stade, le glisser-déposer est entièrement implémenté. Vous pouvez donc faire glisser des photos depuis l'application Files vers Socialite.
Améliorons son apparence en ajoutant une bordure visuelle pour indiquer que la zone peut accepter des éléments déposés.
Pour ce faire, nous pouvons utiliser des hooks supplémentaires qui correspondent aux différentes étapes de la session de glisser-déposer :
onStarted()
: appelé lorsqu'une session de glisser-déposer commence et queDragAndDropTarget
peut recevoir des éléments. C'est un bon endroit pour préparer l'état de l'interface utilisateur pour la session entrante.onEntered()
: déclenché lorsqu'un élément déplacé entre dans les limites de ceDragAndDropTarget
.onMoved()
: appelé lorsque l'élément déplacé se déplace dans les limites deDragAndDropTarget
.onExited()
: appelé lorsque l'élément déplacé sort des limites deDragAndDropTarget
.onChanged()
: appelé lorsque quelque chose change dans la session de glisser-déposer, dans les limites de cette cible (par exemple, si une touche de modification est enfoncée ou relâchée).onEnded()
: appelé à la fin de la session de glisser-déposer. ToutDragAndDropTarget
ayant précédemment reçu un événementonStarted
recevra ce hook. Utile pour réinitialiser l'état de l'interface utilisateur.
Pour ajouter la bordure visuelle, nous devons procéder comme suit :
- Créer une variable booléenne mémorisée qui est définie sur
true
au début d'une opération de glisser-déposer, puis rétablie surfalse
lorsqu'elle se termine. - Appliquer un modificateur au composable
MessageList
qui affiche une bordure lorsque cette variable esttrue
.
override fun onEntered(event: DragAndDropEvent) {
super.onEntered(event)
isDraggedOver = true
}
override fun onEnded(event: DragAndDropEvent) {
super.onExited(event)
isDraggedOver = false
}
6. Raccourcis clavier
$ git checkout codelab-adaptive-apps-step-3
Lorsqu'ils utilisent une application de chat sur ordinateur, les utilisateurs s'attendent à retrouver des raccourcis clavier qui leurs sont familiers, par exemple, envoyer un message avec la touche Entrée.
Au cours de cette étape, nous allons ajouter ce comportement à notre application.
Les événements de clavier dans Compose sont gérés avec des modificateurs.
Il en existe deux principaux :
onPreviewKeyEvent
: intercepte l'événement de clavier avant qu'il ne soit traité par l'élément sélectionné. Lors de l'implémentation, nous décidons de propager l'événement ou de l'utiliser.onKeyEvent
: intercepte l'événement de clavier après qu'il a été traité par l'élément sélectionné. Il ne se déclenche que si les autres gestionnaires n'ont pas utilisé l'événement.
Dans notre cas, l'utilisation de onKeyEvent
sur un TextField
ne fonctionnerait pas, car le gestionnaire par défaut utilise l'événement de touche Entrée et déplace le curseur vers la nouvelle ligne.
.onPreviewKeyEvent { keyEvent ->
//TODO: implement key event handling
},
Le lambda dans le modificateur sera appelé deux fois pour chaque frappe : une fois lorsque l'utilisateur appuie sur la touche et une fois lorsqu'il la relâche.
Nous pouvons déterminer lequel en vérifiant la propriété type
de l'objet KeyEvent
. L'objet d'événement expose également des indicateurs de modification, y compris :
isAltPressed
isCtrlPressed
isMetaPressed
isShiftPressed
Le renvoi de true
à partir du lambda informe Compose que notre code a traité l'événement de touche et empêche le comportement par défaut, comme l'insertion d'une nouvelle ligne.
Implémentez maintenant le modificateur onPreviewKeyEvent
. Vérifiez que l'événement correspond à la touche Entrée et qu'aucun des modificateurs Maj, Alt, Ctrl ou Meta n'est appliqué. Appelez ensuite la fonction onSendClick()
.
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.key == Key.Enter && keyEvent.type == KeyEventType.KeyDown
&& keyEvent.isShiftPressed == false
&& keyEvent.isAltPressed == false
&& keyEvent.isCtrlPressed == false
&& keyEvent.isMetaPressed == false) {
onSendClick()
true
} else {
false
}
},
7. Menus contextuels
$ git checkout codelab-adaptive-apps-step-4
Les menus contextuels sont des éléments importants d'une interface utilisateur adaptative.
Au cours de cette étape, nous allons ajouter un menu pop-up Répondre qui s'affiche lorsque l'utilisateur effectue un clic droit sur un message.
De nombreux gestes sont déjà compatibles. Par exemple, le modificateur clickable
permet de détecter facilement un clic.
Pour les gestes personnalisés, tels que les clics droit, nous pouvons utiliser le modificateur pointerInput
, qui nous donne accès aux événements de pointeur bruts et un contrôle total sur la détection des gestes.
Commençons par ajouter l'interface utilisateur qui répondra à un clic droit. Dans notre cas, nous souhaitons afficher DropdownMenu
avec un seul élément : un bouton Répondre. Nous aurons besoin de deux variables remember
:
rightClickOffset
stocke la position du clic afin que nous puissions déplacer le bouton Répondre à proximité du curseur.isMenuVisible
contrôle l'affichage ou le masquage du bouton Répondre
Leurs valeurs seront mises à jour dans le cadre de la gestion du geste de clic droit.
Nous devons également encapsuler le composable de message dans un Box
afin que le DropdownMenu
puisse apparaître par-dessus.
@Composable
internal fun MessageBubble(
...
) {
var rightClickOffset by remember { mutableStateOf<DpOffset>(DpOffset.Zero) }
var isMenuVisible by remember { mutableStateOf(false) }
val density = LocalDensity.current
Box(
modifier = Modifier
.pointerInput(Unit) {
// TODO: Implement right click handling
}
.then(modifier),
) {
AnimatedVisibility(isMenuVisible) {
DropdownMenu(
expanded = true,
onDismissRequest = { isMenuVisible = false },
offset = rightClickOffset,
) {
DropdownMenuItem(
text = { Text("Reply") },
onClick = {
// Custom Reply functionality
},
)
}
}
MessageBubbleSurface(
...
) {
...
}
}
}
Implémentons maintenant le modificateur pointerInput
. Tout d'abord, nous ajoutons awaitEachGesture
, qui lance un nouveau champ d'application chaque fois que l'utilisateur commence un nouveau geste. Dans ce champ d'application, nous devons :
- Obtenir l'événement de pointeur qui suit :
awaitPointerEvent()
fournit un objet représentant l'événement de pointeur. - Filtrer pour un clic droit pur : nous vérifions que seul le bouton secondaire est enfoncé.
- Capturer la position du clic : prenez la position en pixels et convertissez-la en
DpOffset
pour que le placement du menu soit indépendant du nombre de points par pouce - Afficher le menu : définissez
isMenuVisible
surtrue
et conservez le décalage afin queDropdownMenu
s'affiche exactement à l'emplacement du pointeur. - Consommer l'événement : appelez
consume()
en appuyant et en relâchant les touches correspondantes, ce qui empêche les autres gestionnaires de réagir.
.pointerInput(Unit) {
awaitEachGesture { // Start listening for pointer gestures
val event = awaitPointerEvent()
if (
event.type == PointerEventType.Press
&& !event.buttons.isPrimaryPressed
&& event.buttons.isSecondaryPressed
&& !event.buttons.isTertiaryPressed
// all pointer inputs just went down
&& event.changes.fastAll { it.changedToDown() }
) {
// Get the pressed pointer info
val press = event.changes.find { it.pressed }
if (press != null) {
// Convert raw press coordinates (px) to dp for positioning the menu
rightClickOffset = with(density) {
isMenuVisible = true // Show the context menu
DpOffset(
press.position.x.toDp(),
press.position.y.toDp()
)
}
}
// Consume the press event so it doesn't propagate further
event.changes.forEach {
it.consume()
}
// Wait for the release and consume it as well
waitForUpOrCancellation()?.consume()
}
}
}
8. Félicitations
Félicitations ! Vous avez migré l'application vers Navigation 3 et ajouté les éléments suivants :
- Mises en page adaptatives
- Glisser-déposer
- Raccourcis clavier
- Menu contextuel
Vous disposez d'une base solide pour créer une application entièrement adaptative.
En savoir plus