Étapes clés pour améliorer l'accessibilité de Compose

Pour aider les personnes ayant des besoins d'accessibilité à utiliser votre application, concevez-la de sorte qu'elle réponde aux principales exigences d'accessibilité.

Définissez une taille minimale pour les zones cibles tactiles

Tout élément à l'écran sur lequel un utilisateur peut cliquer ou appuyer, ou avec lequel il peut interagir, doit être assez grand pour permettre une interaction fiable. Lorsque vous dimensionnez ces éléments, assurez-vous de définir la taille minimale sur 48 dp afin de respecter les consignes d'accessibilité Material Design.

Les composants Material, tels que Checkbox, RadioButton, Switch, Slider et Surface, définissent cette taille minimale en interne, mais uniquement lorsque le composant peut recevoir des actions de l'utilisateur. Par exemple, lorsque le paramètre onCheckedChange d'un élément Checkbox est défini sur une valeur non nulle, la case à cocher inclut une marge intérieure dont la largeur et la hauteur sont d'au moins 48 dp.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

Lorsque le paramètre onCheckedChange est défini sur "null", la marge intérieure n'est pas incluse, car il est impossible d'interagir directement avec le composant.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Figure 1 Case à cocher sans marge intérieure.

Lorsque vous implémentez des commandes de sélection telles que Switch, RadioButton ou Checkbox, vous levez généralement le comportement cliquable vers un conteneur parent, définissez le rappel de clic du composable sur null et ajoutez un modificateur toggleable ou selectable au composable parent.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

Lorsque la taille d'un composable cliquable est inférieure à la taille minimale de la zone cible tactile, Compose augmente quand même les dimensions de cette zone. Pour ce faire, la taille de la zone cible tactile est étendue en dehors des limites du composable.

L'exemple suivant contient un très petit élément Box cliquable. La zone cible tactile est automatiquement étendue au-delà des limites de la Box. Par conséquent, appuyer à côté de Box déclenche quand même l'événement de clic.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

Pour éviter tout chevauchement éventuel entre les zones tactiles de différents composables, utilisez toujours une taille minimale suffisamment grande pour le composable. Dans cet exemple, cela impliquerait d'utiliser le modificateur sizeIn pour définir la taille minimale de la zone intérieure:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

Ajouter des étiquettes de clic

Vous pouvez utiliser une étiquette de clic pour ajouter une signification sémantique au comportement de clic d'un composable. Les libellés de clic décrivent ce qui se passe lorsque l'utilisateur interagit avec le composable. Les services d'accessibilité utilisent des libellés de clic pour décrire l'application aux utilisateurs ayant des besoins spécifiques.

Définissez le libellé du clic en transmettant un paramètre dans le modificateur clickable:

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

Si vous n'avez pas accès au modificateur cliquable, définissez le libellé de clic dans le modificateur semantics:

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

Décrire les éléments visuels

Lorsque vous définissez un composable Image ou Icon, le framework Android ne dispose d'aucun moyen automatique de comprendre ce que l'application affiche. Vous devez transmettre une description textuelle de l'élément visuel.

Imaginez un écran sur lequel l'utilisateur peut partager la page active avec ses amis. Cet écran contient une icône de partage cliquable :

Une bande d'icônes cliquables, avec l'icône

En se basant uniquement sur l'icône, le framework Android ne peut pas la décrire à un utilisateur malvoyant. Le framework Android a besoin d'une description textuelle supplémentaire de l'icône.

Le paramètre contentDescription décrit un élément visuel. Utilisez une chaîne localisée, car elle est visible par l'utilisateur.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Certains éléments visuels sont purement décoratifs et vous ne souhaitez peut-être pas les communiquer à l'utilisateur. Lorsque vous définissez le paramètre contentDescription sur null, vous indiquez au framework Android que cet élément n'est associé à aucune action ni à aucun état.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

C'est à vous de décider si un élément visuel spécifique a besoin d'une contentDescription. Demandez-vous si l'élément transmet les informations dont l'utilisateur aura besoin pour effectuer sa tâche. Si ce n'est pas le cas, il est préférable de laisser la description de côté.

Fusionner des éléments

Les services d'accessibilité tels que TalkBack et Switch Access permettent aux utilisateurs de déplacer le curseur sur les différents éléments à l'écran. Il est important que les éléments soient sélectionnés avec la précision appropriée. Lorsque chaque composable de bas niveau sur l'écran est sélectionné indépendamment, les utilisateurs doivent beaucoup interagir pour se déplacer sur l'écran. Si les éléments fusionnent de manière trop agressive, les utilisateurs risquent de ne pas comprendre quels éléments vont ensemble.

Lorsque vous appliquez un modificateur clickable à un composable, Compose fusionne automatiquement tous les éléments qu'il contient. Cela s'applique également à ListItem. Les éléments d'un élément de liste fusionnent et les services d'accessibilité les voient comme un seul élément.

Il se peut qu'un ensemble de composables constitue un groupe logique, mais que ce groupe ne soit pas cliquable ou ne fasse pas partie d'un élément de liste. Vous souhaitez toujours que les services d'accessibilité les voient comme un seul élément. Par exemple, imaginez un composable qui affiche l'avatar d'un utilisateur, son nom et des informations supplémentaires:

Groupe d'éléments d'interface utilisateur comprenant un nom d'utilisateur. Le nom est sélectionné.

Vous pouvez autoriser Compose à fusionner ces éléments à l'aide du paramètre mergeDescendants dans le modificateur semantics. De cette façon, les services d'accessibilité ne sélectionnent que l'élément fusionné, et toutes les propriétés sémantiques des descendants sont fusionnées.

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date} • ${metadata.readTimeMinutes} min read")
        }
    }
}

Les services d'accessibilité se concentrent désormais sur l'ensemble du conteneur en une seule fois, en fusionnant leur contenu:

Groupe d'éléments d'interface utilisateur comprenant un nom d'utilisateur. Tous les éléments sont sélectionnés ensemble.

Ajouter des actions personnalisées

Examinez l'élément de liste suivant :

Élément de liste typique reprenant le titre de l'article, l'auteur et l'icône des favoris.

Lorsque vous utilisez un lecteur d'écran tel que TalkBack pour entendre ce qui est affiché à l'écran, celui-ci sélectionne d'abord l'élément entier, puis l'icône des favoris.

L'élément de liste, avec tous les éléments sélectionnés ensemble.

L'élément de liste, avec seulement l'icône de favori sélectionnée

Dans une longue liste, cela peut devenir très répétitif. Une meilleure approche consiste à définir une action personnalisée qui permet à un utilisateur d'ajouter l'élément à ses favoris. N'oubliez pas que vous devez également supprimer explicitement le comportement de l'icône de favori pour vous assurer qu'elle n'est pas sélectionnée par le service d'accessibilité. Pour ce faire, utilisez le modificateur clearAndSetSemantics:

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

Décrire l'état d'un élément

Un composable peut définir une stateDescription pour la sémantique utilisée par le framework Android pour lire l'état dans lequel se trouve le composable. Par exemple, un composable activable peut être à l'état "coché" ou "non coché". Dans certains cas, vous pouvez remplacer les libellés de description d'état par défaut utilisés par Compose. Pour ce faire, spécifiez explicitement les étiquettes de description d'état avant de définir un composable comme activable:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Définir des titres

Les applications affichent parfois beaucoup de contenu sur un même écran dans un conteneur déroulant. Par exemple, un écran peut afficher le contenu complet d'un article que l'utilisateur lit :

Capture d'écran d'un article de blog, avec le texte de l'article dans un conteneur à faire défiler.

Les utilisateurs ayant des besoins en matière d'accessibilité ont des difficultés à naviguer sur un tel écran. Pour faciliter la navigation, indiquez quels éléments sont des en-têtes. Dans l'exemple précédent, chaque titre de sous-section pourrait être défini comme un titre pour l'accessibilité. Certains services d'accessibilité, tels que TalkBack, permettent aux utilisateurs de passer directement d'un titre à l'autre.

Dans Compose, vous indiquez qu'un composable est un titre en définissant sa propriété semantics:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Gérer les composables personnalisés

Chaque fois que vous remplacez certains composants Material de votre application par des versions personnalisées, vous devez garder à l'esprit les considérations d'accessibilité.

Supposons que vous remplacez le Checkbox Material par votre propre implémentation. Vous pouvez oublier d'ajouter le modificateur triStateToggleable, qui gère les propriétés d'accessibilité de ce composant.

En règle générale, examinez l'implémentation du composant dans la bibliothèque Material et imitez tous les comportements d'accessibilité que vous pouvez trouver. Utilisez également de manière intensive les modificateurs de base plutôt que les modificateurs de niveau de l'interface utilisateur, car ils incluent des éléments d'accessibilité prêts à l'emploi.

Testez l'implémentation de vos composants personnalisés avec plusieurs services d'accessibilité pour vérifier leur comportement.

Ressources supplémentaires