Rendre son application accessible avec Jetpack Compose

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é.

Smartphone montrant les touches de volume

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.

Illustration des gestes d'utilisation de 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 Icon sans contentDescription).
  • Les descriptions d'éléments en double.
L'Accessibility Scanner encadre les zones non conformes sur une capture d'écran de l'application et au clique, affiche les problèmes rencontrés
Source : Accessibility scanner - Accessibility on Android

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é").
Sur Android Studio, en haut à droite de l'interface ce trouve le bouton "Toggle Layout Inspector" qui permet d'afficher des attributs de l'élément sélectionné
Source : Android Developers - Déboguer votre mise en page avec l'outil d'inspection de la mise en page

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.
Sur Android Studio, dans l'interface des Preview ce trouve un bouton "Start UI Check Mode" permettant d'activer la fonctionnalité
Sur Android Studio, lorsque l'UI Check est activé, il affichera le résultat de ses analyses en bas de l'interface
Source : Android Developers - Tester l'accessibilité de votre application

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

Formulaires avec des erreurs sur les champs de saisies avec une bordure rouge, une icône d'erreur et un texte informant sur la nature de l'erreur

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 en dp. Cela permet au texte de s'adapter aux réglages de taille de police du système de l'utilisateur.
  • Utilisez le MaterialTheme.typography pour 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 les Layouts fluides (comme Column et Row) qui permettent aux composants de grandir verticalement pour accommoder le texte plus grand.
Exemple d'interface Material 3 non conforme au zoom à 200%
Source : Material 3 - Foundations

// ❌ 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. Un label (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 label de OutlinedTextField ou TextField.
  • Utiliser le paramètre supportingText pour afficher les contraintes. Quand isError = true, supportingText doit afficher le message d'erreur. Compose lie automatiquement ce message au champ via la sémantique error().
  • Utiliser le trailingIcon du TextField pour y placer un IconButton qui bascule l'état d'un VisualTransformation (entre PasswordVisualTransformation() et VisualTransformation.None).
  • Utiliser rememberSaveable au lieu de remember pour stocker l'état du texte.
Formulaire utilisant le Design System de l'État, qui respect les critères d'accessibilité
Source : Système de Design de l’État

// ❌ 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 les IconButton. 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 Image ou une Box).
  • 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 Row avec un titre et une description ou les Card) pour éviter les swipes inutiles.
  • Exemple :
Un élément Card de Material 3

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 Text comme 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
Application avec une page d'article de blog

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.clickable le fait pour vous. Mais si vous utilisez un détecteur de geste bas niveau (comme pointerInput), vous devez spécifier l'action manuellement pour que les lecteurs d’écrans annoncent "Appuyez deux fois pour activer". Peut aussi être utilisé surcharger le onClick pour 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) ou LiveRegionMode.Assertive (interrompt l'annonce en cours).
  • Exemple : Une dialog d’alerte
Pop up d'alerte Material 3

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
Bottom sheet Material 3

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ètre supportingText quand isError = 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 : contentDescription décrit ce que fait le champ ("Sélectionner la date de naissance"), tandis que error dé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, CircularProgressIndicator et Slider le 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 (le LazyColumn lui-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 bloc items { ... }.
    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 : LazyColumn et LazyVerticalGrid gè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 un Column avec un forEach.
Page d'accueil de l'application Uber Eats avec un filtre représenté par une liste d'élément statique

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 un traversalIndex plus bas (ex: 1f) sont lus avant les éléments avec un traversalIndex plus élevé (ex: 2f).
    La plupart des éléments ont un traversalIndex de 0f par défaut.
  • Exemple : Un formulaire complexe créé avec Box où 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