Aller au contenu

Accessibilité Mobile avec Jetpack Compose

Découvrez l'accessibilité Mobile avec JetPack Compose ! Un élément clé pour toute application mobile.

Photo by Sigmund / Unsplash - personne utilisant un clavier spécifique pour l'accessibilité

L'accessibilité est un élément clé pour toute application mobile, visant à améliorer l'expérience utilisateur et à toucher un public plus large. Cet article propose une exploration approfondie du sujet.

Qu'est-ce que l'accessibilité ?

L'accessibilité dans le développement d'applications mobiles se définit comme l'aptitude à être utilisée par le plus grand nombre, y compris les personnes ayant des handicaps (moteurs, auditifs, visuels ou cognitifs). Les handicaps peuvent être situationnels, temporaires ou permanents.

L'importance de l'accessibilité

Dans le monde des applications mobiles, trois principaux modes d'interaction se distinguent :

  • Le Tactile (Tap)

L'interaction tactile est la forme la plus courante de navigation dans les applications mobiles, permettant aux utilisateurs de contrôler l'interface avec des gestes simples.

Talkback offre une expérience vocale interactive, permettant aux utilisateurs malvoyants de naviguer dans les applications en écoutant des descriptions des éléments à l'écran.

VoiceAccess permet aux utilisateurs de contrôler entièrement leur appareil mobile à l'aide de commandes vocales, rendant la technologie plus accessible aux personnes ayant des limitations physiques.

SwitchAccess fournit une méthode alternative d'interaction pour les utilisateurs ayant des difficultés motrices, en leur permettant de naviguer dans les applications à l'aide de commutateurs au lieu de l'écran tactile.

Conception et développement d'une application Android accessible

Cadre Android pour l'accessibilité

L'accessibilité débute avec le framework Android, qui analyse l'application et la traduit de façon à être adaptée aux besoins des utilisateurs. Le framework génère un arbre de nœuds représentant l'affichage à l'écran. Chaque nœud est un AccessibilityNodeInfo, comprenant des détails tels que le libellé (Label) et les actions possibles (Tap, Click...). Cela permet aux services d'accessibilité, tels que Talkback et Voice Access, de naviguer aisément et de vocaliser les libellés des composants.

Jetpack Compose, outil UI pour Android

Jetpack Compose est un ensemble d'outils UI pour le développement Android. Lors de la création d'une fonction composable, une composition est effectuée pour afficher les éléments à l'écran. Une partie de cette composition consiste à créer un arbre sémantique définissant la signification des éléments.

Éléments clés pour une accessibilité optimale :

1. Description du Contenu

Dans Jetpack Compose, il est crucial de fournir des descriptions pour les éléments interactifs. Utilisez Modifier.semantics pour ajouter des descriptions accessibles.

Exemple : Pour un texte envoyer, vous pouvez ajouter :

Text(
    text = "Envoyer", 
    modifier = Modifier.semantics { 
        contentDescription = "Envoyer le Formulaire"
        role = Role.Button
    }
)

Comme cela, on explique aux services d'accessibilité que ce texte est bien cliquable et joue bien le rôle d'un bouton. Comme cela, talkback va prononcer :

"Envoyer le formulaire Appuyez deux fois pour activer"

2. Description de l'État

La description de l'état informe sur le statut actuel des éléments UI.

Exemple : Pour un interrupteur (Switch), indiquez si elle est activée ou non.

Dans le cas où l'on ne gère pas l'accessibilité, on aura :

@Composable
fun SwitchExemple() {
    var selected by remember { mutableStateOf(false) }
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .background(Color.LightGray)
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Text(text = "Task")
        Checkbox(
            checked = selected,
            onCheckedChange = { selected = !selected },
            modifier = Modifier.align(Alignment.CenterVertically)
        )
    }
}

Dans ce cas, avec Talkback on passer par le text Task puis naviguer vers le checkbox
Talkback va prononcer :
non coché. Case à cocher

Appuyer deux fois pour cocher ou désélectionner

Lorsqu'on tap deux fois : coché

Pour une meilleure expérience, on pourrait agrandir le callback de onCheckedChange au parent pour qu'il soit tout visible comme seul élement au niveau de lecteur d'écran.

Ensuite pour gérer l'état de checkbox, on pourrait définir dans stateDescription deux états "terminé" et "en cours" selon le booléen selected pour donner une description dynamique et claire sur l'état de la tâche.

@Composable
fun SwitchExemple() {
    var selected by remember { mutableStateOf(false) }
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .semantics {
                stateDescription = if (selected) {
                    "terminé"
                } else {
                    "en cours"
                }
            }
            .toggleable(
                value = selected,
                onValueChange = { selected = !selected },
                role = Role.Checkbox
            )
            .background(Color.LightGray)
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Text(text = "Task")
        Checkbox(
            checked = selected,
            onCheckedChange = null,
            modifier = Modifier.align(Alignment.CenterVertically)
        )
    }
}

3. Taille de Texte Dynamique

Permettez l'ajustement de la taille du texte selon la configuration actuelle en fonction de l'échelle de la police (fontScale) du système

Pour détecter que l'utilisateur est en mode avec affichage plus large, on vérifie le fontScale de la configuration actuelle. S'il est supérieur à 1.0f(la taille system par défaut) on pourrait penser a réajuster la taille d'un composant Text par exemple.

Dans le code ci-dessous on récupère un taille de texte selon la fontScale de la configuration actuelle : si elle est supérieur à 1.2f c'est à dire plus larde de 20% de la taille par défaut.

@Composable
fun textResizableBasedOnConfig() {
    Text(
        text = "name",
        fontSize = getFontSizeBasedOnCurrentScale(
            configuration = LocalConfiguration.current,
            fontSize = 14.sp
        )
    )
}

fun getFontSizeBasedOnCurrentScale(configuration: Configuration, fontSize: TextUnit) =
    if (configuration.fontScale > 1.2f) {
        (fontSize.value / configuration.fontScale).sp
    } else {
        TextUnit.Unspecified
    }

4. Rôle

Définissez le rôle des composants pour une meilleure compréhension.

Il ya plusieurs rôles qui sont prédéfinis et qui simplifient la définition des composants. Par exemple, pour un bouton de la bibliothèque jetpack compose, il est déjà déclaré avec un Role.Button.

C'est la même chose pour les rôles : Checkbox, Button, DropDownList, Image, Tab, RadioButton, Switch

Par contre, il est important de définir le rôle pour les composants custom et des composants avec des rôles personnalisés.

Exemple : Pour un box clickable, indiquez clairement le rôle si cela comporte comme bouton :

@Composable
fun RoleExemple() {
    Box(
        modifier = Modifier.clickable {}
        .semantics { role = Role.Button }) {
        Text("Click Me")
    }
}

5. Libellé de Clic

Fournissez des libellés explicatifs pour les actions de clic.

Exemple : Pour un bouton de click sur un post, précisez l'action :

@Composable
fun CustomClickable() {
     Row(
        Modifier.clickable(
            onClickLabel = "Lire le post",
            onClick = {}
        )
    )
}

6. Titre

Avec la fonction heading, on déclare que ce composable est déclaré comme titre. Cela permet de faciliter la compréhension de l'écran et facilite la navigation en cas de plusieurs titres dans la même page, avec talkback. Par exemple, il est possible de naviguer de titre en titre sans entrer dans le détail de chaque section.

@Composable
fun HeadingText() {
    Text(
        "Title",
        style = MaterialTheme.typography.h1,
        modifier = Modifier.semantics { heading() }
    )
}

7. LiveRegion

Il y a des cas d'utilisation où on a besoin de lire des changements de manière dynamique sans mettre le focus sur l'élément. On peut citer comme exemple le nombre d'article à acheter : on trouve à la page de la liste des commandes des boutons avec une icône plus ou moins pour ajouter et diminuer le nombre des articles.

Avec Talkback, lorsqu'on est sur le bouton, on ne voit pas et on n'entend pas le numéro d'article courant. C'est pour cela que l'on pourra utiliser le liveRegion qui se présente en deux modes :

  • Polite : d'une manière poli Talkback continue à prononcer ce qu'il est en train de dire puis va dire la description concerné par liveRegion.
  • Assertive : Talkback va prononcer directement la description concerné même s'il est en train de prononcer autre chose.
@Composable
private fun liveRegionExemple() {
    var i by remember { mutableStateOf(0) }
    Text(text = "count $i",
        modifier = Modifier.semantics {
            liveRegion = LiveRegionMode.Polite
            contentDescription = "current count is $i"
        }
    )
    Button(onClick = {
        i += 1
    }) {
        Text("add")
    }
}

Dans ce cas, lorsqu'on clique sur le bouton en utilisant Talkback, ce dernier va dire directement "current count is 1" sans besoin de revenir avec le focus sur le champs précédent pour connaître le nouveau nombre.

8.  Fusion des Descendants

Avec mergeDescendants, on pourrait grouper des éléments de même conteneur pour que cela se prononce qu'une seule fois. Dans l'exemple ci-dessous, l'utilisateur va entendre se prononcer le nom "Nilson Loic" puis, il va devoir naviguer au composant suivant pour arriver à la profession "Architect Cloud". Avec mergeDescendants, Talkback va prononcer "Nilson Loic, Architect Cloud" en une seule fois 😎.

@Composable
fun ComplexComponent() {
    Column(
            modifier = Modifier.semantics(mergeDescendants = true) {},
        ) {
            Text(text = "Nilson Loic")
            Text(
                color = Color.Gray,
                text = "Architect Cloud"
            )
        }
}

9. ClearAndSet

Jetpack compose propose aussi de modifier clearAndSetSemantics qui permet de supprimer les sémantiques de tous les descendants et déclare une nouvelle sémantique. On pourrait distinguer deux exemples :
Le premier est de forcer un composant connu par sa sémantique à être invisible par les services d'accessibilité. Prenons l'exemple d'un bouton : si on considère que ce n'est pas important que talkback le prononce, on pourrait faire :

@Composable
fun ButtonInvisibleParTalkback(){
    Button(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = {
            //
        }) {
        Text("Button invisible pour talkback")
    }
}

Pour le deuxième exemple, on pourrait citer un conteneur contenant plusieurs éléments décoratifs, on pourrait supprimer les sémantiques des descendants et décaler une sémantique claire sur le parent :

@Composable
fun clearAndSetExemple() {
    Row(
        modifier = Modifier
            .clickable(
                onClick = {
                    Log.e(TAG, "clicked")
                }
            )
            .clearAndSetSemantics {
                contentDescription = "Lire l'article"
                role = Role.Button
            }
    ) {
        Text(
            text = "Text non visible avec talkback à cause de clearAndSet",
        )
    }
}


10. Gestion du Focus

Gérez le focus sur les éléments interactifs pour faciliter la navigation.

isTraversalGroup

@Composable
fun TraversalGroup() {
    Row {
        Column(modifier = Modifier.weight(0.5f)) {
            Text(text = "Quantité")
            Text(text = "10")
            Modifier.semantics { isTraversalGroup = true }
        }
        Column(modifier = Modifier.weight(0.5f)) {
            Text(text = "Total")
            Text(text = "100")
            Modifier.semantics { isTraversalGroup = true }
        }
    }
}

Ce bout de code affiche l'image ci-dessus. Si on considère le code sans les sémantiques, Talkback va prononcer les éléments de haut vers le bas, de gauche à droite de la manière :
Quantité->Total->10-> 100
Il est évident que cette lecture n'est pas la bonne. isTraversalGroup vient pour résoudre ce problème. Il déclare le groupe lié sémantiquement, ce qui met la prononciation du premier groupe Quantité et 10 ensemble, puis passe au deuxième groupe Total 100.

Traversal index

Utiliser traversalIndex en conjonction avec isTraversalGroup dans Jetpack Compose permet un contrôle plus nuancé de l'ordre de navigation pour les lecteurs d'écran tels que TalkBack. Avec traversalIndex, les éléments avec des valeurs plus basses sont priorisés. La valeur par défaut est 0f et peut être positif ou négatif.

Voici un exemple illustrant comment utiliser ces propriétés pour améliorer l'accessibilité de votre application. On retrouve sur l'écran, dans l'ordre le 1er, 3eme et 2eme élément. Par contre, talkback va lire les éléments dans l'ordre correct le 1er, 2eme et puis le 3eme texte grâce à traversalIndex 👌🏼.

@Composable
fun traversalIndexExemple() {
    Column(
        modifier = Modifier.semantics { isTraversalGroup = true },
    ) {
        Text(
            text = "1er",
            modifier = Modifier.semantics { traversalIndex = 0f },
        )
        Text(
            text = "3eme",
            modifier = Modifier.semantics { traversalIndex = 2f },
        )
        Text(
            text = "2eme",
            modifier = Modifier.semantics { traversalIndex = 1f },
        )
    }
}


11. Actions personnalisées

Parfois, on a beaucoup d'actions organisé de manière complexe en terme d'UI, ce qui rend la navigation en terme d'accessibilité beaucoup plus difficile. Une idée pour résoudre cela est de regrouper les actions sémantiques dans une liste d'actions. Du point de vue Talkback, lorsque le focus se met sur le texte "Plus d'actions", Talkback va prononcer :

Et lorsque l'on tape avec trois doigts, on aura la liste d'actions dans une modale et on pourrait naviguer facilement sur la liste des actions 😉

@Composable
fun CustomAccessibilityActionExemple() {
    Text(
        "Plus d'actions",
        modifier = Modifier.semantics {
            customActions = listOf(
                CustomAccessibilityAction("J'aime") {
                    true
                },
                CustomAccessibilityAction("Comment") {
                    true
                },
                CustomAccessibilityAction("Partage") {
                    true
                }
            )
        }
    )
}

Conclusion

L'accessibilité est essentielle dans le développement d'applications mobiles pour garantir une expérience utilisateur inclusive. Cet article a exploré diverses stratégies en utilisant Jetpack Compose, telles que la gestion des descriptions de contenu, des états et plus encore. Bien que des progrès significatifs aient été réalisés, il reste des domaines à améliorer, notamment la sensibilisation et la formation continues des développeurs. En intégrant l'accessibilité dès le début du processus de développement, nous pouvons créer des applications qui sont non seulement fonctionnelles mais aussi accessibles à tous, contribuant ainsi à un avenir numérique plus inclusif.

Pour commencer n’hésitez pas à commencer par ce codelab https://developer.android.com/codelabs/jetpack-compose-accessibility?hl=fr#0

Dernier