La puissance et les avantages de Jetpack Compose dans un projet Android ne sont plus à prouver. Mais saviez-vous qu'il facilite aussi énormément la prise en compte de l'accessibilité ?
En tant que développeurs, on se concentre souvent sur le design, la qualité de notre code ou l'architecture de notre projet, et l'accessibilité n’est pas forcément une tâche priorisée à sa juste valeur. Pourtant, nous avons une part de responsabilité importante dans ces enjeux. Notre code a un impact direct et fort pour aider les personnes en situation de handicap, notamment visuel.
Et pour nous aider dans cette tâche, nous disposons d'une palette d'outils divers.
Dans cet article nous verrons les grands axes sous lesquels aborder l’accessibilité avec la raison qui motive ces principes, leurs règles associés ainsi que des exemples en Compose.
Les aides techniques
Talkback
TalkBack est le lecteur d'écran officiel de Google, intégré directement sur Android. Il permet aux utilisateurs de naviguer dans l'interface sans la voir car il vocalise tout ce qui est sélectionné.
Pour activer TalkBack : Accédez à Paramètres > Accessibilité > TalkBack pour activer TalkBack. Pour activer le raccourci TalkBack, accédez à Paramètres > Accessibilité et activez le raccourci via les touches de volume. Vous pouvez ensuite maintenir les touches de volume enfoncées pendant quelques secondes pour activer ou désactiver TalkBack.

Source icône : Image de Freepik - Icônes de mains
Pour explorer l’écran et énoncer l’élément qui est sélectionné : Appuyer ou faire glisser d’un doigt
Pour accéder à l’élément suivant : Balayer vers la droite ou la gauche
Pour activer l’élément sélectionné : Appuyer deux fois
Pour faire défiler à l’écran vers le haut/bas : Balayer vers le haut ou vers le bas avec deux doigts
Pour configurer le geste balayer vers le haut/bas : Balayer l'écran avec trois doigts vers le haut/bas ou balayer l'écran vers le haut, puis vers le bas (sur les appareils non compatibles avec les gestes à plusieurs doigts (antérieurs à Android 11 avec TalkBack 9.1)
Pour accéder à l'élément suivant en utilisant le paramètre de contrôle de la lecture : Balayer vers le haut ou vers le bas avec un doigt
Personnaliser les contrôles de lecture
Les contrôles de lecture suivants sont disponibles par défaut :
- Caractères : permet de lire un caractère à la fois.
- Mots : permet de lire un mot à la fois.
- Lignes : permet de lire une ligne à la fois.
- Paragraphes : permet de lire un paragraphe à la fois.
- Titres : permet d'aller au titre suivant ou précédent, s'il en existe un.
- Commandes : permet de passer d'un élément actif au suivant ou au précédent, par exemple des boutons, des cases à cocher ou des champs de texte.
- Liens : permet de passer au lien suivant ou précédent.
- Vitesse d'élocution : permet de modifier la vitesse d'élocution de TalkBack.
- Pour l'accélérer : balayez l'écran vers le haut.
- Pour la ralentir : balayez l'écran vers le bas.
- Langue parlée : permet de changer la langue de la synthèse vocale si vous en avez installé plusieurs.
- Rechercher sur l'écran : permet de rechercher un terme sur l'écran.
Accessibility Scanner
L'Accessibility Scanner est une application de Google disponible sur le PlayStore qui permet d’analyser l’accessibilité d’une application à partir d’une capture d'écran. Il analyse les problèmes d'accessibilité statiques comme :
- Les contrastes de couleur insuffisants.
- Les zones cliquables trop petites (moins de 48dp).
- Les labels manquants (une
IconsanscontentDescription). - Les descriptions d'éléments en double.

Outils intégrés à Android Studio
Android Studio contient aussi des outils pour analyser l'accessibilité de nos applications.
Layout Inspector
Lorsque votre application tourne, vous pouvez ouvrir le “Layout Inspector" en bas de l'IDE. Vous pouvez cliquer sur un composant et voir toutes ses propriétés sémantiques :
- Vérifier le
contentDescription. - Vérifier le
mergeDescendants. - Confirmer le
role(ex :Bouton,Case à cocher, etc.). - Inspecter le
stateDescription(ex : "Coché").

L’UI Check
Dans la fenêtre d’aperçu (@Preview) de vos @Composable, vous pouvez activer le “UI Check Mode” et voir les problèmes courants :
- Vérifier les contrastes de couleur.
- Vérifier la taille des zones cliquables.
- Vérifier les
contentDescription.


Quelques bonnes pratiques en vrac
Lorsque l’on a des PDF dans notre application, laisser les utilisateurs ouvrir le PDF avec le logiciel de leur choix car les logiciels spécialisés ont déjà leur propre gestion de l’accessibilité.
Attention à l’écriture inclusive : le point médian n’est pas bien retranscrit dans les lecteurs d’écran.
Porter l’attention de l’utilisateur sur le contenu important et minimiser les distractions.
Découper les processus en étapes logique et essentiel en indiquant leur progression.
Faciliter la navigation en utilisant des titres reconnus par les lecteurs d’écrans et en réduisant le nombre de gestes à faire pour passer d’un contenu à un autre.
Les contenus audiovisuels comme les vidéos doivent contenir la possibilité d’être accompagnés de sous-titres pour rendre ses contenus accessibles pour les personnes avec un handicap auditif.
Maintenant, rentrons dans le détail des grands principes d’accessibilité.
Images
Principe : Les images véhiculant des informations utiles pour les utilisateurs ou les images qui permettent d’effectuer une action doivent être accompagnées d’un texte alternatif. En opposition, les images décoratives doivent être ignorées.
Raison : Les utilisateurs naviguant avec un lecteur d'écran ne peuvent pas voir l'image. Le texte alternatif est le seul moyen de leur vocaliser l'information essentielle ou la fonction de cet élément, tandis qu'ignorer les images décoratives évite de créer du "bruit" inutile dans leur parcours.
Règle : Le texte alternatif doit décrire l’information de manière concise en expliquant la fonction de l’illustration et non son type (inutile de dire s’il s’agit d’une image, d’une photo, d’une icône, etc).
Avec Compose :
// Associer un texte alternatif
Image(
painter = /*...*/,
contentDescription = "Ajouter aux favoris",
)
// Ignorer une image décorative
Image(
painter = /*...*/,
contentDescription = null,
)
Design
Contrastes de couleur
Principe : Le texte et les éléments interactifs doivent avoir un contraste suffisant avec leur arrière-plan.
Raison : Les utilisateurs malvoyants ont besoin de contrastes élevés afin d’être en capacité de lire le texte.
Règle : Viser un ratio de 4.5:1 pour le texte normal (texte de taille inférieure à 32 sp) et 3:1 pour le grand texte (texte de taille supérieure à 32 sp) ou les icônes. WCAG - Règle 1.4 Distinguable : faciliter la perception visuelle et auditive du contenu par l'utilisateur, notamment en séparant le premier plan de l'arrière-plan
Avec Compose : Utilisez des outils comme "Accessibility Scanner" ou des vérificateurs de contraste en ligne lors de la définition de vos couleurs primary, onPrimary, surface, onSurface, etc.
Utilisation de la couleur
Principe : Ne jamais utiliser la couleur comme unique moyen de transmettre une information (ex: un champ en erreur juste bordé de rouge).
Raison : Les utilisateurs malvoyants (notamment les daltoniens) ne verront pas la différence et n’auront pas accès à une information importante qui pourrait les bloquer dans leur parcours utilisateur.
Règle : Toujours doubler l'information avec du texte ou une icône indicative.
Avec Compose : Si un champ est en erreur, ajoutez une icône (ex: Icons.Filled.Error) et un texte d'aide (ex: "Champ obligatoire").
Source image : Text fields Guidelines
// ❌ Problème : Seule la couleur change
TextField(
value = text,
onValueChange = { text = it },
isError = true // Change juste la couleur (souvent en rouge)
)
// ✅ Solution : Ajout d'infos non-basées sur la couleur
TextField(
value = text,
onValueChange = { text = it },
isError = true,
trailingIcon = { // Icône
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Icône d'erreur"
)
},
supportingText = { // Texte d'aide
Text(text = "Ce champ ne peut pas être vide")
}
)
Typographie
Principe : Le texte doit être lisible, en utilisant des polices claires, un espacement suffisant et une taille suffisamment grande.
Raison : Une typographie claire bénéficie à tous, mais elle est cruciale pour les utilisateurs malvoyants ou ceux présentant des troubles de la lecture (comme la dyslexie), en réduisant la fatigue cognitive.
Règle : Préférez les polices sans empattement (sans-serif, ex: Roboto) et utiliser une taille de police minimale de 12 sp pour le corps du texte.
Avec Compose :
- Définissez toujours vos tailles de police (
fontSize) en**sp**(Scale-independent Pixels) et jamais endp. Cela permet au texte de s'adapter aux réglages de taille de police du système de l'utilisateur. - Utilisez le
MaterialTheme.typographypour définir vos styles (headlineLarge, titleMedium, bodySmall, etc.) et appliquez-les de manière cohérente dans l'application.
Mise en page
Principe : L'application doit rester lisible et fonctionnelle lorsque l'utilisateur augmente la taille de la police via les paramètres d'accessibilité système.
Raison : Les utilisateurs malvoyants dépendent de cette fonctionnalité pour grossir le texte à une taille qui leur convient. Si l'interface ne s’adapte pas, ils ne peuvent tout simplement pas utiliser l'application.
Règle : L'interface utilisateur doit s'adapter sans tronquer de texte, sans chevauchement d'éléments et sans perte de fonctionnalité, jusqu'à 200% de la taille de police par défaut (conformément aux directives WCAG). Android Developers - Mise à l'échelle non linéaire de la police à 200 %
Avec Compose :
- Éviter les conteneurs avec des hauteurs fixes (ex:
Modifier.height(48.dp)) qui contiennent du texte. - Préférez les hauteurs minimales (ex:
Modifier.heightIn(min = 48.dp)) ou lesLayoutsfluides (commeColumnetRow) qui permettent aux composants de grandir verticalement pour accommoder le texte plus grand.

// ❌ Problème : Si la taille de la police système est à 200%,
// le texte sera coupé verticalement car la Row est fixée à 56dp.
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp) // Hauteur fixe
) {
Text("Titre de la section qui peut être long")
}
// ✅ Solution : Utiliser 'heightIn' pour définir une hauteur minimale
// tout en permettant au conteneur de s'agrandir si nécessaire.
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp) // Hauteur minimale
) {
Text("Titre de la section qui peut être long")
Taille des zones cliquables
Principe : Les zones interactives (boutons, items de liste, icônes) doivent être suffisamment grandes.
Raison : Ce sont des éléments importants qui doivent être utilisés facilement, notamment par les utilisateurs ayant des troubles moteurs.
Règle : La taille minimale recommandée est de 48dp x 48dp pour Android (44pt x 44pt pour iOS). Material 3 - Touch and pointer target sizes
Avec Compose : Les composants Material (comme Button, IconButton) gèrent souvent cela. Pour les icônes ou les zones cliquables personnalisées, forcez la taille minimale.
// ❌ Problème : L'icône fait 24dp, la zone cliquable aussi.
Icon(
imageVector = Icons.Filled.Info,
contentDescription = "Plus d'infos",
modifier = Modifier.clickable { /* ... */ }
)
// ✅ Solution 1 : Utiliser le bon composant
IconButton(onClick = { /* ... */ }) { // IconButton gère un padding pour atteindre 48dp
Icon(Icons.Filled.Info, "Plus d'infos")
}
// ✅ Solution 2 : Forcer la taille (si IconButton n'est pas adapté)
Icon(
imageVector = Icons.Filled.Info,
contentDescription = "Plus d'infos",
modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp) // Garantit la taille min
.clickable { /* ... */ }
)
Formulaires
Principe : Les formulaires doivent guider activement l'utilisateur, être tolérants aux erreurs et ne jamais faire perdre d'informations.
Raison : Les formulaires peuvent être des points de frictions. Des instructions peu claires, des erreurs mal communiquées ou la perte de données saisies pénalisent fortement les utilisateurs, en particulier ceux ayant des troubles de l'attention, des troubles moteurs (saisie difficile) ou utilisant un lecteur d'écran.
Règle :
- Utiliser un label : Un
placeholder(texte indicatif dans le champ) ne suffit pas, car il disparaît lors de la saisie. Unlabel(libellé) visible en permanence est nécessaire. - Annoncer les contraintes : Informer l'utilisateur des règles de saisie (ex: "Mot de passe de 8 caractères minimum") avant qu'il ne valide.
- Gérer les erreurs : Les erreurs doivent être communiquées visuellement (couleur, icône) et textuellement (message d'aide).
- Permettre la vérification : Pour les champs “mot de passe”, laisser la possibilité d’afficher ou de masquer la saisie.
- Préserver la saisie : L'état des champs doit être conservé si l'utilisateur change d'écran ou pivote l'appareil.
Avec Compose :
- Toujours utiliser le paramètre
labeldeOutlinedTextFieldouTextField. - Utiliser le paramètre
supportingTextpour afficher les contraintes. QuandisError = true,supportingTextdoit afficher le message d'erreur. Compose lie automatiquement ce message au champ via la sémantiqueerror(). - Utiliser le
trailingIconduTextFieldpour y placer unIconButtonqui bascule l'état d'unVisualTransformation(entrePasswordVisualTransformation()etVisualTransformation.None). - Utiliser
rememberSaveableau lieu derememberpour stocker l'état du texte.

// ❌ Problème : Placeholder seul, pas de message d'erreur clair, état non sauvegardé
var email by remember { mutableStateOf("") }
TextField(
value = email,
onValueChange = { email = it },
placeholder = { Text("Votre email") } // Disparaît à la saisie
// Aucune info d'erreur
)
// ✅ Solution : Label, supportingText pour l'erreur, et rememberSaveable
var email by rememberSaveable { mutableStateOf("") } // Sauvegarde la valeur
val isError = email.isNotEmpty() && !email.contains("@")
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Votre email") }, // Reste visible
isError = isError, // Gère l'erreur sémantiquement
supportingText = {
if (isError) {
// Annoncé par TalkBack comme l'erreur du champ
Text("Le format de l'email est invalide")
}
},
modifier = Modifier.fillMaxWidth()
)
// Afficher ou masquer la saisie
var isVisible by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(
label = {
Text("Mot de passe")
},
value = password,
onValueChange = {
updatePassword(it)
},
leadingIcon = {
ShowPasswordButton(isVisible = isVisible) {
isVisible = !isVisible
}
},
visualTransformation = if (isVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
}
L’accessibilité sur Compose
La sémantique, représentée par Modifier.semantics sur Compose, est l'ensemble des informations qu'un composant fournit aux services d'accessibilité (comme TalkBack) pour décrire ce qu'il est, ce qu'il fait, et son état actuel. Android Developers - Sémantique
La plupart des composants standards (Button, Text, Checkbox) ont déjà une sémantique correcte. Vous devez l'ajuster ou la définir pour les composants personnalisés ou lorsque le comportement par défaut n'est pas assez descriptif.
Les propriétés sémantiques
Voici les propriétés les plus courantes que vous définirez dans un bloc Modifier.semantics { ... } :
contentDescription (Description de contenu) :
- Usage : Le texte lu par le screen reader, c'est la sémantique la plus importante pour les éléments non textuels.
- Contexte : Essentiel pour les
Icon,Image, ou lesIconButton. Permet aussi d’ajouter du contexte à du texte simple ou de remplacer une annonce visuelle ambiguë par une description claire. - Exemple :
// Indique la fonction de l'icône
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Supprimer l'élément"
)
// Clarifie un texte visuel ambigu
Text(
text = "4.5 ⭐️⭐️⭐️⭐️⭐️",
modifier = Modifier.semantics {
// Remplace l'annonce par défaut ("4.5 étoile étoile...")
contentDescription = "Note : 4.5 sur 5"
}
) // Annonce : "Note : 4.5 sur 5"
role (Rôle) :
- Usage : Indique la nature du composant.
- Contexte : Crucial lorsque vous rendez un élément cliquable qui n'est pas un bouton (ex: une
Imageou uneBox). - Valeurs :
Role.Button,Role.Checkbox,Role.Switch,Role.RadioButton,Role.Image,Role.Tab,Role.Heading. - Exemple : Une image qui agit comme un bouton.
Image(
painter = painterResource(R.drawable.my_button_img),
contentDescription = "Lancer la recherche",
modifier = Modifier
.clickable { /* ... */ }
.semantics { role = Role.Button } // Annonce : "Lancer la recherche, Bouton"
)
stateDescription (Description de l'état) :
- Usage : Décrit l'état actuel d'un composant, au-delà de "coché" ou "non coché".
- Contexte : Utile pour les composants personnalisés (ex: un toggle jour/nuit, un accordéon).
- Exemple : Un interrupteur personnalisé.
// (val isChecked: Boolean)
Box(modifier = Modifier.semantics {
role = Role.Switch
// On gère l'état nous-même au lieu de laisser TalkBack dire "coché/non coché"
stateDescription = if (isChecked) "Activé" else "Désactivé"
})
mergeDescendants (Fusionner les descendants) :
- Usage : Indique au lecteur d’écran de lire tous les descendants textuels de ce composant comme une seule phrase, au lieu de naviguer entre eux. Permet de réduire le nombre de gestes à effectuer pour lire le contenu d’un composant/d’un écran.
- Contexte : Extrêmement utile pour les items de liste (ex: une
Rowavec un titre et une description ou lesCard) pour éviter les swipes inutiles. - Exemple :
Source image : Card Guidelines
// ❌ Problème : TalkBack lit "Glass Soul's World Tour", swipe, "From your recent favorites", swipe, "..."
Card(Modifier.clickable { }) {
Text("Glass Soul's World Tour")
Text("From your recent favorites")
Text("...")
}
// ✅ Solution : Lit "Glass Soul's World Tour, From your recent favorites, ..." en un seul focus.
Row(Modifier
.clickable { }
.semantics(mergeDescendants = true) { }
) {
Text("Glass Soul's World Tour")
Text("From your recent favorites")
Text("...")
}
clearAndSetSemantics (Effacer et définir la sémantique) :
- Usage : Ignore toute la sémantique des enfants et la remplace par celle que vous définissez.
- Contexte : Très puissant. À utiliser lorsque les enfants sont trop complexes ou non pertinents (ex: un graphique personnalisé, un calendrier) et que vous voulez donner une description résumée.
- Exemple :
// Les enfants (Box, Text...) décrivant les barres du graphique sont ignorés
MyComplexChart(
modifier = Modifier.clearAndSetSemantics {
contentDescription = "Graphique des ventes. Ventes de 20€ en Janvier, 45€ en Février."
}
)
heading (Titre) :
- Usage : Marque un
Textcomme un titre. - Contexte : Permet aux utilisateurs de lecteurs d’écrans de naviguer rapidement de titre en titre.
- Exemple : Un article composé de plusieurs paragraphes séparés par des titres
Source image : Accessibility semantics
@Composable
private fun Subsection(text: String) {
Text(
text = text,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics { heading() }
)
}
onClick (Action de clic) :
- Usage : Ajoute une "action" sémantique.
- Contexte :
Modifier.clickablele fait pour vous. Mais si vous utilisez un détecteur de geste bas niveau (commepointerInput), vous devez spécifier l'action manuellement pour que les lecteurs d’écrans annoncent "Appuyez deux fois pour activer". Peut aussi être utilisé surcharger leonClickpour ajouter un label qui modifie la fin du texte “Appuyer 2 fois pour …” - Exemple :
IconButton(
onClick = { /* ... */ },
modifier = Modifier.semantics {
// On change le label de l'action
onClick(label = "Supprimer l'élément", action = null)
}
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Corbeille"
)
}
// Annonce TalkBack : "Corbeille, Bouton. Appuyez deux fois pour supprimer l'élément."
liveRegion (Région "live") :
- Usage : Indique aux lecteurs d’écrans d'annoncer automatiquement les changements de contenu de ce composant, même s'il n'a pas le focus.
- Contexte : Parfait pour les alertes ou un pop-up, comme un
Snackbar, les minuteurs, ou les scores qui se mettent à jour. - Propriétés :
LiveRegionMode.Polite(attend la fin de l'annonce en cours) ouLiveRegionMode.Assertive(interrompt l'annonce en cours). - Exemple : Une dialog d’alerte
Source : Dialogs Guidelines
PopupAlert(
message = "You have a new message",
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite
}
)
disabled (Désactivé) :
- Usage : Marque explicitement un composant comme désactivé.
- Contexte : Les composants Material (ex:
Button(enabled = false)) le font. Utilisez-le si vous avez un composant cliquable personnalisé que vous désactivez visuellement. - Exemple :
Box(
modifier = Modifier.semantics { if (isActuallyDisabled) disabled() }
)
paneTitle (Titre de Volet) :
- Usage : Définit un titre pour un "volet" distinct de l'écran.
- Contexte : Très important dans les mises en page adaptatives (tablettes, pliables) ou les
ModalBottomSheet. Lorsqu'un utilisateur navigue dans un nouveau volet, le lecteur d’écran doit annoncer où il vient d'atterrir. - Exemple : Une modal pour partager un contenu
Source : Bottom sheets Guidelines
ModalBottomSheet(onDismissRequest = { /* ... */ }) {
Column(
modifier = Modifier.semantics {
// Annonce "Partager" dès que la BottomSheet s'ouvre
paneTitle = "Partager"
}
) {
// ...
}
}
error (Erreur) :
- Usage : Marque un composant comme étant dans un état d'erreur et fournit le message d'erreur associé.
- Contexte : C'est la sémantique utilisée par
TextField(via le paramètresupportingTextquandisError = true). Vous devez l'utiliser si vous créez un composant de formulaire personnalisé (ex: un sélecteur de date personnalisé) qui peut échouer à la validation. - Note :
contentDescriptiondécrit ce que fait le champ ("Sélectionner la date de naissance"), tandis queerrordécrit ce qui ne va pas ("La date ne peut pas être dans le futur"). - Exemple : Champ personnalisé
// (val selectedDate: LocalDate?, val errorMessage: String?)
MyCustomDatePicker(
selectedDate = selectedDate,
modifier = Modifier.semantics {
if (errorMessage != null) {
// Marque le composant en erreur
isError()
// Fournit le message d'erreur à TalkBack
error(errorMessage)
}
}
)
progressBarRangeInfo (Infos sur la plage de la barre de progression) :
- Usage : Décrit l'état d'une barre de progression (ou d'un
Slider) en donnant la valeur actuelle, la plage (min/max) et le nombre d'étapes (optionnel). - Contexte : Les composants
LinearProgressIndicator,CircularProgressIndicatoretSliderle gèrent pour vous. Utilisez-le si vous créez un indicateur de progression ou un slider personnalisé. - Exemple : Indicateur de volume personnalisé
// (val currentVolume: Float) où currentVolume est entre 0.0f et 1.0f
MyCustomVolumeBar(
progress = currentVolume,
modifier = Modifier.semantics {
progressBarRangeInfo = ProgressBarRangeInfo(
current = currentVolume * 100,
range = 0f..100f,
steps = 10 // S'ajuste par paliers de 10%
)
}
)
// Annonce TalkBack : "Barre de progression, 75%. 10 étapes."
// (Note: Si `steps` n'est pas fourni, il est traité comme continu)
collectionInfo et collectionItemInfo (Infos de Collection) :
- Ces deux sémantiques fonctionnent ensemble. Elles sont cruciales pour les listes et les grilles (
LazyColumn,LazyRow,LazyVerticalGrid). Elles permettent à TalkBack d'annoncer "Item 3 sur 10" au lieu de juste "Item". collectionInfo(Infos sur la Collection)
Usage : Appliqué au conteneur de la liste/grille (leLazyColumnlui-même).
Propriétés :rowCount(nombre de lignes),columnCount(nombre de colonnes).collectionItemInfo(Infos sur l'Item de Collection)
Usage : Appliqué à chaque item dans le blocitems { ... }.
Propriétés :rowIndex(index de ligne),columnIndex(index de colonne),rowSpan(combien de lignes l’item occupe),columnSpan(combien de colonnes l’item occupe).- Contexte :
LazyColumnetLazyVerticalGridgèrent cela automatiquement. Vous n'avez pas besoin de les ajouter manuellement si vous utilisez ces composants. - Avec Compose : Si vous construisez une liste ou une grille personnalisée sans utiliser les composants
Lazy, par exemple, en utilisant unColumnavec unforEach.
Source : Application UberEats
// ❌ Problème : Une liste faite "à la main"
// TalkBack ne sait pas combien il y a d'items.
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
myItems.forEachIndexed { index, item ->
MyItemRow(item)
}
}
// ✅ Solution : Ajouter manuellement la sémantique de collection
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.semantics {
// 1. Sémantique du conteneur
collectionInfo = CollectionInfo(
rowCount = myItems.size,
columnCount = 1 // C'est une liste (1 colonne)
)
}
) {
myItems.forEachIndexed { index, item ->
MyItemRow(
item = item,
modifier = Modifier.semantics {
// 2. Sémantique de l'item
collectionItemInfo = CollectionItemInfo(
rowIndex = index,
rowSpan = 1,
columnIndex = 0,
columnSpan = 1
)
}
)
}
}
// Annonce TalkBack en se concentrant sur le 3ème item :
// "[Contenu de l'item], 3 sur [N]..."
traversalIndex (Index de Parcours) :
- Usage : Permet de forcer un ordre de focus spécifique pour les services d'accessibilité, en remplaçant l'ordre de parcours par défaut.
- Contexte : C'est une propriété à utiliser avec extrême prudence. Dans 99% des cas, si l'ordre de focus est incorrect, c'est que votre hiérarchie de layout (ex:
Column,Row) est sémantiquement incorrecte. Il vaut mieux corriger le layout que de patcher l'ordre avec traversalIndex.
Les éléments avec untraversalIndexplus bas (ex:1f) sont lus avant les éléments avec untraversalIndexplus élevé (ex:2f).
La plupart des éléments ont untraversalIndexde0fpar défaut. - Exemple : Un formulaire complexe créé avec
Boxoù les champs ne sont pas dans l'ordre logique de haut en bas.
// ❌ Problème : L'ordre de lecture de TalkBack est 1, puis 3, puis 2.
Box(modifier = Modifier.fillMaxSize()) {
Text(
"Étape 1: Votre nom",
modifier = Modifier
.align(Alignment.TopStart)
.semantics { isTraversalGroup = true } // Groupe pour le focus
)
Text(
"Étape 3: Confirmation", // Visuellement en dessous de l'étape 2
modifier = Modifier
.align(Alignment.Center)
.semantics { isTraversalGroup = true }
)
Text(
"Étape 2: Votre email", // Visuellement au-dessus de l'étape 3
modifier = Modifier
.align(Alignment.CenterStart)
.semantics { isTraversalGroup = true }
)
}
// Annonce TalkBack (swipe) : "Étape 1...", "Étape 3...", "Étape 2..." (incorrect)
// ✅ Solution (Patch avec traversalIndex) :
Box(modifier = Modifier.fillMaxSize()) {
Text(
"Étape 1: Votre nom",
modifier = Modifier
.align(Alignment.TopStart)
.semantics { traversalIndex = 1f; isTraversalGroup = true } // 1er
)
Text(
"Étape 3: Confirmation",
modifier = Modifier
.align(Alignment.Center)
.semantics { traversalIndex = 3f; isTraversalGroup = true } // 3ème
)
Text(
"Étape 2: Votre email",
modifier = Modifier
.align(Alignment.CenterStart)
.semantics { traversalIndex = 2f; isTraversalGroup = true } // 2ème
)
}
// Annonce TalkBack (swipe) : "Étape 1...", "Étape 2...", "Étape 3..." (corrigé)
Conclusion
En résumé :
L'accessibilité visuelle : C'est la base de tout. Les contrastes doivent être suffisants, les polices des textes sont en sp et les zones cliquables respectent la taille minimale de 48dp.
L'arbre sémantique est notre allié : C’est de cette façon que nos applications communiquent avec les lecteurs d’écrans. Utilisez Modifier.semantics pour le sculpter.
L’accessibilité est une pratique à intégrer dans nos habitudes de développement dès l’écriture des premières lignes.
Pour mieux comprendre les enjeux d’accessibilité
L’accessibilité appliquée au mobile
L’accessibilité numérique et le design inclusif
Nouveau palier pour le numérique responsable avec l’European Accessibility Act
Sources
Support Google - Utiliser les gestes TalkBack
Android Developers - Sémantique
Android Developers - Tester l'accessibilité de votre application
