1. Introduction
Dans cet atelier de programmation, vous apprendrez à utiliser Jetpack Compose pour améliorer l'accessibilité de votre application. Vous découvrirez, étape par étape, plusieurs cas d'utilisation courants. Nous aborderons les tailles des zones cibles tactiles, les descriptions de contenu, les libellés de clic, etc.
Les personnes malvoyantes, daltoniennes, malentendantes ou présentant des troubles de la dextérité, cognitifs ou toute autre forme de handicap utilisent des appareils Android pour effectuer des tâches quotidiennes. Lorsque vous développez des applications en gardant l'accessibilité à l'esprit, vous améliorez l'expérience utilisateur pour ces personnes, mais aussi pour celles ayant d'autres besoins.
Au cours de cet atelier de programmation, nous utiliserons TalkBack pour tester manuellement les modifications du code. TalkBack est un service d'accessibilité principalement utilisé par les déficients visuels. Assurez-vous de tester également les modifications apportées à votre code auprès d'autres services d'accessibilité, tels que Switch Access.
TalkBack en action dans l'application Jetnews
Points abordés
Cet atelier de programmation traite des points suivants :
- Répondre aux besoins des utilisateurs souffrant d'un handicap en augmentant la taille des zones cibles tactiles
- Décrire les propriétés sémantiques et expliquer comment les modifier
- Fournir des informations aux composables pour améliorer leur accessibilité
Ce dont vous avez besoin
- Connaissances de la syntaxe du langage Kotlin, y compris les lambdas.
- Expérience de base avec Compose. Il peut être utile de suivre l'atelier de programmation sur les principes de base de Jetpack Compose avant cet atelier de programmation.
- Vous disposez d'un appareil ou d'un émulateur Android sur lequel TalkBack est activé.
Objectifs de l'atelier
Dans cet atelier de programmation, nous allons améliorer l'accessibilité d'une application de lecture d'actualités. Nous commencerons avec une application qui ne dispose pas de certaines fonctionnalités d'accessibilité essentielles et nous appliquerons ce que nous avons appris pour la rendre plus accessible aux personnes ayant des besoins spécifiques.
2. Configuration
Au cours de cette étape, vous téléchargerez le code requis, qui comprend une application de lecture d'actualités simple.
Ce dont vous avez besoin
Obtenir le code
Le code de cet atelier de programmation est disponible dans le dépôt GitHub codelab-android-compose. Pour le cloner, exécutez la commande suivante :
$ git clone https://github.com/android/codelab-android-compose
Vous pouvez également télécharger deux fichiers ZIP :
Découvrir l'application exemple
Le dépôt que vous venez de télécharger contient du code pour tous les ateliers de programmation traitant de Compose. Pour cet atelier, ouvrez le projet AccessibilityCodelab
dans Android Studio.
Nous vous recommandons de commencer par le code de la branche main
, puis de suivre l'atelier étape par étape, à votre propre rythme.
Configurer TalkBack
Au cours de cet atelier de programmation, nous utiliserons TalkBack pour vérifier nos modifications. Si vous utilisez un appareil physique pour effectuer les tests, suivez ces instructions afin d'activer TalkBack. TalkBack n'est pas installé par défaut avec les émulateurs. Choisissez un émulateur qui inclut le Play Store, puis téléchargez les outils d'accessibilité Android.
3. Taille des cibles tactiles
Tous les éléments affichés sur lesquels on peut cliquer ou appuyer, ou avec lesquels il est possible d'interagir, doivent être assez grands pour permettre des interactions fiables. Assurez-vous que ces éléments font au moins 48 dp de large et de haut.
Si ces commandes sont dimensionnées de façon dynamique ou redimensionnées en fonction de la taille de leur contenu, vous pouvez utiliser le modificateur sizeIn
pour définir la limite inférieure de leurs dimensions.
Certains composants Material définissent ces tailles automatiquement. Par exemple, le paramètre MinHeight
du composable Button est défini sur 36 dp et utilise une marge verticale de 8 dp, ce qui correspond à la hauteur requise de 48 dp.
Lorsque nous ouvrons l'application exemple et que nous exécutons TalkBack, nous pouvons voir que la zone cible tactile de l'icône en forme de croix figurant sur les fiches d'article est très petite. Elle devrait être d'au moins 48 dp.
Voici une capture d'écran montrant l'application d'origine (à gauche) et une version améliorée (à droite).
Examinons l'implémentation et vérifions la taille de ce composable. Ouvrez PostCards.kt
et recherchez le composable PostCardHistory
. Comme vous pouvez le constater, l'implémentation définit une taille de 24 dp pour l'icône du menu à développer :
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer),
modifier = Modifier
.clickable { openDialog = true }
.size(24.dp)
)
}
}
// ...
}
Pour augmenter la taille de la zone cible tactile de cet élément Icon
, nous pouvons ajouter une marge intérieure :
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer),
modifier = Modifier
.clickable { openDialog = true }
.padding(12.dp)
.size(24.dp)
)
}
}
// ...
}
Dans notre cas d'utilisation, il existe un moyen plus simple de s'assurer que la zone cible tactile est d'au moins 48 dp. En effet, le composant Material IconButton
gère cela pour nous :
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
Désormais, si vous parcourez l'écran avec TalkBack, une zone cible tactile de 48 dp s'affiche. En outre, IconButton
ajoute une ondulation, qui indique à l'utilisateur que cet élément est cliquable.
4. Libellés de clic
Par défaut, les éléments cliquables dans votre application ne fournissent aucune information sur les effets d'un clic sur ces éléments. Par conséquent, les services d'accessibilité tels que TalkBack utilisent une description très générique par défaut.
Pour offrir une expérience optimale aux utilisateurs ayant des besoins d'accessibilité spécifiques, nous pouvons fournir une description précise qui explique ce qui se passe lorsqu'un utilisateur clique sur cet élément.
Dans l'application Jetnews, les utilisateurs peuvent cliquer sur les différentes fiches pour lire l'article complet. Par défaut, cette action entraîne la lecture du contenu de l'élément cliquable, qui se trouve après le texte "Appuyer deux fois pour activer le contenu". Nous aimerions être plus précis et remplacer ce texte par "Appuyer deux fois pour lire l'article". Voici la version originale par rapport à la version souhaitée :
Modifier le libellé de clic d'un composable : avant (à gauche) et après (à droite)
Le modificateur clickable
inclut un paramètre qui vous permet de définir directement ce libellé de clic.
Examinons de nouveau l'implémentation de PostCardHistory
:
@Composable
fun PostCardHistory(
// ...
) {
Row(
Modifier.clickable { navigateToArticle(post.id) }
) {
// ...
}
}
Comme vous pouvez le voir, cette implémentation utilise le modificateur clickable
. Pour définir un libellé de clic, nous pouvons définir le paramètre onClickLabel
:
@Composable
fun PostCardHistory(
// ...
) {
Row(
Modifier.clickable(
// R.string.action_read_article = "read article"
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
}
}
TalkBack énonce désormais correctement "Appuyer deux fois pour lire l'article".
Les autres fiches d'article publiées sur l'écran d'accueil portent le même libellé générique. Examinons l'implémentation du composable PostCardPopular
et mettons à jour son libellé de clic :
@Composable
fun PostCardPopular(
// ...
) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier.size(280.dp, 240.dp),
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
Ce composable utilise le composable Card
en interne, ce qui ne vous permet pas de définir directement le libellé de clic. À la place, vous pouvez utiliser le modificateur semantics
pour définir ce libellé :
@Composable
fun PostCardPopular(
post: Post,
navigateToArticle: (String) -> Unit,
modifier: Modifier = Modifier
) {
val readArticleLabel = stringResource(id = R.string.action_read_article)
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier
.size(280.dp, 240.dp)
.semantics { onClick(label = readArticleLabel, action = null) },
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
5. Actions personnalisées
De nombreuses applications affichent une liste, où chaque élément contient une ou plusieurs actions. Sur un lecteur d'écran, il peut s'avérer fastidieux de parcourir une telle liste, car la même action serait sélectionnée encore et encore.
À la place, nous pouvons ajouter des actions d'accessibilité personnalisées à un composable. De cette façon, les actions associées au même élément de la liste peuvent être regroupées.
Dans l'application Jetnews, nous affichons une liste d'articles que l'utilisateur peut lire. Chaque élément de la liste inclut une action permettant d'indiquer que l'utilisateur souhaite voir moins d'articles sur ce sujet. Dans cette section, nous allons remplacer cette action par une action d'accessibilité personnalisée afin de faciliter la navigation dans la liste.
Sur la gauche, vous voyez la situation par défaut où chaque icône en forme de croix est sélectionnable. Sur la droite, vous voyez la solution, où l'action est incluse dans les actions personnalisées de TalkBack :
Ajouter une action personnalisée à un article : avant (à gauche) et après (à droite)
Ouvrez PostCards.kt
et examinez l'implémentation du composable PostCardHistory
. Notez les propriétés cliquables de Row
et de IconButton
, qui utilisent Modifier.clickable
et onClick
:
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
Modifier.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
Par défaut, les composables Row
et IconButton
sont cliquables et sont donc rendus sélectionnables par TalkBack. Ce comportement se répète pour chaque élément de la liste. Vous devez donc balayer l'écran beaucoup lorsque vous parcourez la liste. Nous voulons que l'action liée à IconButton
soit incluse en tant qu'action personnalisée au niveau de l'élément de la liste. Pour indiquer aux services d'accessibilité de ne pas interagir avec cet élément Icon
, utilisez le modificateur clearAndSetSemantics
:
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
Modifier.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(
modifier = Modifier.clearAndSetSemantics { },
onClick = { openDialog = true }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
Cependant, en supprimant la sémantique de IconButton
, il n'est plus du tout possible d'exécuter l'action. À la place, nous pouvons ajouter l'action à l'élément de la liste en insérant une action personnalisée dans le modificateur semantics
:
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
val showFewerLabel = stringResource(R.string.cd_show_fewer)
Row(
Modifier
.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = showFewerLabel,
// action returns boolean to indicate success
action = { openDialog = true; true }
)
)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(
modifier = Modifier.clearAndSetSemantics { },
onClick = { openDialog = true }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = showFewerLabel
)
}
}
}
// ...
}
Nous pouvons désormais utiliser le pop-up d'action personnalisée dans TalkBack pour appliquer l'action. Cette pratique est particulièrement pertinente lorsque le nombre d'actions dans un élément de liste augmente.
6. Description des éléments visuels
Tous les utilisateurs de votre application ne peuvent pas voir ni interpréter les éléments visuels qui s'y affichent, comme les icônes et les illustrations. De plus, les services d'accessibilité n'ont aucun moyen d'interpréter les éléments visuels en se basant uniquement sur leurs pixels. En tant que développeur, vous devez donc leur transmettre plus d'informations sur les éléments visuels de votre application.
Les composables visuels comme Image
et Icon
incluent un paramètre contentDescription
. C'est dans ce paramètre que vous transmettez une description localisée de cet élément visuel, ou la valeur null
si l'élément est purement décoratif.
Dans notre application, il manque des descriptions de contenu sur l'écran de l'article. Exécutez l'application et sélectionnez l'article du haut pour accéder à l'écran correspondant.
Ajouter une description du contenu visuel : avant (à gauche) et après (à droite)
Lorsque nous ne fournissons aucune information, l'icône de navigation dans la partie supérieure gauche annonce simplement "Bouton, appuyez deux fois pour l'activer". L'utilisateur ne sait pas quelle action aura lieu lorsqu'il cliquera sur ce bouton. Ouvrez ArticleScreen.kt
:
@Composable
fun ArticleScreen(
// ...
) {
// ...
Scaffold(
topBar = {
InsetAwareTopAppBar(
title = {
// ...
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null
)
}
}
)
}
) {
// ...
}
}
Ajoutez une description de contenu pertinente à l'icône :
@Composable
fun ArticleScreen(
// ...
) {
// ...
Scaffold(
topBar = {
InsetAwareTopAppBar(
title = {
// ...
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(
R.string.cd_navigate_up
)
)
}
}
)
}
) {
// ...
}
}
L'image d'en-tête est un autre élément visuel de cet article. Dans notre cas, cette image est purement décorative et ne montre rien que nous devions communiquer à l'utilisateur. Par conséquent, la description du contenu est définie sur null
, et l'élément est ignoré lorsque nous utilisons un service d'accessibilité.
Le dernier élément visuel à l'écran est la photo de profil. Dans ce cas, nous utilisons un avatar générique. Il n'est donc pas nécessaire d'ajouter une description de contenu ici. Si nous utilisions la photo de profil réelle de cet auteur, nous pourrions lui demander de fournir une description de contenu adaptée.
7. Titres
Lorsqu'un écran contient beaucoup de texte, comme l'écran de notre article, il est assez difficile pour les utilisateurs malvoyants de trouver rapidement la section qu'ils recherchent. Pour ce faire, vous pouvez indiquer les parties du texte qui correspondent à des titres. Les utilisateurs peuvent alors parcourir rapidement les différents titres en balayant l'écran vers le haut ou vers le bas.
Par défaut, aucun composable n'est marqué comme titre. Dès lors, aucune navigation n'est possible. Nous aimerions que l'écran de l'article affiche la navigation par titre :
Ajouter des titres : avant (à gauche) et après (à droite)
Les titres de notre article sont définis dans PostContent.kt
. Ouvrons ce fichier et faisons-le défiler jusqu'au composable Paragraph
:
@Composable
private fun Paragraph(paragraph: Paragraph) {
// ...
Box(modifier = Modifier.padding(bottom = trailingPadding)) {
when (paragraph.type) {
// ...
ParagraphType.Header -> {
Text(
modifier = Modifier.padding(4.dp),
text = annotatedString,
style = textStyle.merge(paragraphStyle)
)
}
// ...
}
}
}
Ici, l'élément Header
est défini comme un composable Text
simple. Nous pouvons définir la propriété sémantique heading
pour indiquer que ce composable est un titre.
@Composable
private fun Paragraph(paragraph: Paragraph) {
// ...
Box(modifier = Modifier.padding(bottom = trailingPadding)) {
when (paragraph.type) {
// ...
ParagraphType.Header -> {
Text(
modifier = Modifier.padding(4.dp)
.semantics { heading() },
text = annotatedString,
style = textStyle.merge(paragraphStyle)
)
}
// ...
}
}
}
8. Fusion personnalisée
Comme nous l'avons vu aux étapes précédentes, les services d'accessibilité tels que TalkBack parcourent un écran élément par élément. Par défaut, chaque composable de bas niveau dans Jetpack Compose qui définit au moins une propriété sémantique est sélectionnable. Par exemple, un composable Text
définit la propriété sémantique text
et devient donc sélectionnable.
Toutefois, un nombre excessif d'éléments sélectionnables peut prêter à confusion, car l'utilisateur doit les parcourir un par un. À la place, vous pouvez fusionner des composables à l'aide du modificateur semantics
avec sa propriété mergeDescendants
.
Consultons l'écran de notre article. Le niveau de sélectionnabilité de la plupart des éléments est correct. Toutefois, les métadonnées de l'article sont actuellement lues à voix haute en tant qu'éléments distincts. Pour améliorer l'expérience utilisateur, vous pouvez les fusionner en une seule entité sélectionnable :
Fusionner des composables : avant (à gauche) et après (à droite)
Ouvrez PostContent.kt
et vérifiez le composable PostMetadata
:
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
Nous pouvons demander à la ligne de premier niveau de fusionner ses descendants, ce qui entraînera le comportement souhaité :
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row(Modifier.semantics(mergeDescendants = true) {}) {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
9. Boutons bascules et cases à cocher
L'état des éléments à activer/désactiver, comme Switch
et Checkbox
, est lu à voix haute à mesure qu'ils sont sélectionnés par TalkBack. Sans contexte, il peut toutefois être difficile de comprendre ce à quoi ces éléments font référence. Pour ajouter du contexte à ce type d'élément basculant, vous pouvez agrandir la zone d'activation et de désactivation. De la sorte, l'utilisateur peut activer ou désactiver Switch
ou Checkbox
en appuyant sur le composable lui-même ou sur son libellé.
Un exemple est illustré dans l'écran "Centres d'intérêt". Pour y accéder, ouvrez le panneau de navigation à partir de l'écran d'accueil. L'écran "Centres d'intérêt" présente la liste des sujets auxquels un utilisateur peut s'abonner. Par défaut, les cases de cet écran sont sélectionnables en dehors de leur libellé, ce qui ne facilite pas leur compréhension. Nous préférerions que l'ensemble de la ligne (Row
) soit activable :
Utiliser des cases à cocher : avant (à gauche) et après (à droite)
Ouvrez InterestsScreen.kt
et examinez l'implémentation du composable TopicItem
:
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = { onToggle() },
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
Comme vous pouvez le voir ici, Checkbox
possède un rappel onCheckedChange
qui gère l'activation et la désactivation de l'élément. Nous pouvons agrandir ce rappel pour qu'il s'applique à toute la ligne (Row
) :
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
10. Description des états
À l'étape précédente, nous avons agrandi la zone d'activation et de désactivation d'un élément Checkbox
pour qu'elle s'applique à la ligne parent (Row
). Pour améliorer encore l'accessibilité de cet élément, vous pouvez ajouter une description personnalisée afin de préciser l'état du composable.
Par défaut, l'état d'une case (Checkbox
) est lu comme "Coché" ou "Non coché". Nous pouvons remplacer cette description par notre propre description personnalisée :
Ajouter des descriptions d'état : avant (à gauche) et après (à droite)
Poursuivons avec le composable TopicItem
que nous avons adapté à la dernière étape :
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
Nous pouvons ajouter nos descriptions d'état personnalisées à l'aide de la propriété stateDescription
dans le modificateur semantics
:
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
val stateSubscribed = stringResource(R.string.state_subscribed)
Row(
modifier = Modifier
.semantics {
stateDescription = if (selected) {
stateSubscribed
} else {
stateNotSubscribed
}
}
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
11. Félicitations !
Félicitations, vous avez terminé cet atelier de programmation au cours duquel vous avez découvert l'accessibilité dans Compose. Vous vous êtes familiarisé avec les zones cibles tactiles, les descriptions d'éléments visuels et les descriptions d'état. Vous avez ajouté des libellés de clic, des titres et des actions personnalisées. Vous savez comment effectuer une fusion personnalisée, et comment utiliser les boutons bascules et les cases à cocher. En appliquant ces enseignements à vos applications, vous améliorerez considérablement leur accessibilité.
Consultez les autres ateliers de programmation du parcours Compose, ainsi que d'autres exemples de code, dont celui de Jetnews.
Documentation
Pour en savoir plus et obtenir des conseils à ce sujet, consultez la documentation suivante :