Sémantique

En plus des informations principales qu'un composable contient, comme une chaîne de texte d'un composable Text, il peut être utile d'obtenir des informations supplémentaires sur les éléments d'interface utilisateur.

Les informations sur le sens et le rôle d'un composant dans Compose sont appelées sémantique. Elles permettent de fournir un contexte supplémentaire sur les composables à des services tels que l'accessibilité, la saisie automatique et les tests. Par exemple, une icône d'appareil photo peut n'être qu'une image, mais sa signification sémantique peut être "Prendre une photo".

En combinant les sémantiques appropriées avec les API Compose appropriées, vous fournissez autant d'informations que possible sur votre composant aux services d'accessibilité, qui décident ensuite de la façon de le représenter à l'utilisateur.

Les API Material et Compose pour l'UI et la fondation sont fournies avec des sémantiques intégrées qui suivent leur rôle et leur fonction spécifiques. Vous pouvez toutefois également modifier ces sémantiques pour les API existantes ou en définir de nouvelles pour les composants personnalisés, en fonction de vos exigences spécifiques.

Propriétés sémantiques

Les propriétés sémantiques expriment la signification du composable correspondant. Par exemple, le composable Text contient une propriété sémantique text, car il s'agit de la signification de ce composable. Un Icon contient une propriété contentDescription (si elle est définie par le développeur) qui indique dans le texte la signification de l'icône.

Réfléchissez à la façon dont les propriétés sémantiques expriment la signification d'un composable. Prenons l'exemple d'un Switch. Voici comment il se présente :

Figure 1. Switch à l'état "Activé" et "Désactivé".

Voici comment vous pouvez décrire la signification de cet élément: "Il s'agit d'un bouton bascule, qui est un élément à activer dont l'état actuel est "Activé". Vous pouvez interagir avec lui en cliquant dessus."

C'est précisément ce à quoi servent les propriétés sémantiques. Le nœud sémantique de cet élément de bouton bascule contient les propriétés suivantes, telles qu'elles sont visualisées avec l'outil d'inspection de la mise en page:

Outil d'inspection de la mise en page affichant les propriétés sémantiques d'un composable "Bouton bascule"
Figure 2. Outil d'inspection de la mise en page affichant les propriétés sémantiques d'un composable Switch.

Role indique le type d'élément. StateDescription décrit comment l'état "Activé" doit être référencé. Par défaut, il s'agit d'une version localisée du mot "On", mais vous pouvez lui donner un nom plus spécifique (p. ex. "Activé") selon le contexte. ToggleableState est l'état actuel du bouton bascule. La propriété OnClick fait référence à la méthode utilisée pour interagir avec cet élément.

Le suivi des propriétés sémantiques de chaque composable dans votre application offre tout un éventail de possibilités puissantes:

  • Les services d'accessibilité utilisent les propriétés pour représenter l'UI affichée à l'écran et permettre aux utilisateurs d'interagir avec elle. Pour le composable Switch, TalkBack peut annoncer: "On" (Activer), "Switch" (Basculer), "Double tap to toggle" (Double appui pour activer/désactiver). L'utilisateur peut appuyer deux fois sur son écran pour désactiver le bouton.
  • Le framework de test utilise les propriétés pour rechercher des nœuds, interagir avec eux et effectuer des assertions:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Les composables et les modificateurs construits sur la bibliothèque de base Compose définissent déjà les propriétés pertinentes pour vous par défaut. Vous pouvez également modifier manuellement ces propriétés pour améliorer la prise en charge de l'accessibilité pour des cas d'utilisation spécifiques, ou modifier la stratégie de fusion ou de suppression de vos composables.

Pour signaler le type de contenu spécifique de votre composant aux services d'accessibilité, vous pouvez appliquer différentes sémantiques. Ces ajouts seront compatibles avec les principales informations sémantiques en place et aideront les services d'accessibilité à affiner la façon dont votre composant est représenté, annoncé ou avec lequel on interagit.

Pour obtenir la liste complète des propriétés sémantiques, consultez l'objet SemanticsProperties. Pour obtenir la liste complète des actions d'accessibilité possibles, consultez l'objet SemanticsActions.

Titres

Les applications contiennent souvent des écrans avec du contenu textuel, comme des articles longs ou des pages d'actualités, qui sont généralement divisés en différentes sous-sections avec des titres:

Article de blog avec le texte de l'article dans un conteneur à faire défiler.
Figure 3. Article de blog avec le texte de l'article dans un conteneur à faire défiler.

Les utilisateurs ayant des besoins spécifiques en termes d'accessibilité peuvent avoir du mal à naviguer facilement sur ce type d'écran. Pour améliorer l'expérience de navigation, certains services d'accessibilité permettent de naviguer plus facilement directement entre les sections ou les titres. Pour activer cette fonctionnalité, indiquez que votre composant est un heading en définissant sa propriété sémantique:

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

Alertes et pop-ups

Si votre composant est une alerte ou un pop-up, comme un Snackbar, vous pouvez signaler aux services d'accessibilité qu'une nouvelle structure ou des mises à jour du contenu peuvent être transmises aux utilisateurs.

Les composants semblables à des alertes peuvent être marqués avec la propriété de sémantique liveRegion. Cela permet aux services d'accessibilité d'informer automatiquement l'utilisateur des modifications apportées à ce composant ou à ses enfants:

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

Vous devez utiliser liveRegionMode.Polite dans la plupart des cas où l'attention des utilisateurs ne doit être attirée que brièvement sur les alertes ou les contenus importants qui changent à l'écran.

Vous devez utiliser liveRegion.Assertive avec parcimonie pour éviter les commentaires perturbateurs. Il doit être utilisé dans les situations où il est essentiel que les utilisateurs soient informés d'un contenu à durée limitée:

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

Les régions en direct ne doivent pas être utilisées pour les contenus qui s'actualisent fréquemment, tels que les compteurs à rebours, afin d'éviter de submerger les utilisateurs de commentaires constants.

Composants ressemblant à des fenêtres

Les composants personnalisés ressemblant à des fenêtres, comme ModalBottomSheet, ont besoin de signaux supplémentaires pour les différencier du contenu environnant. Pour ce faire, vous pouvez utiliser la sémantique paneTitle afin que les modifications de fenêtre ou de volet pertinentes puissent être représentées de manière appropriée par les services d'accessibilité, ainsi que ses principales informations sémantiques:

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

Pour référence, consultez comment Material 3 utilise paneTitle pour ses composants.

Composants d'erreur

Pour d'autres types de contenus, tels que les composants ressemblant à des erreurs, vous pouvez développer les informations sémantiques principales pour les utilisateurs ayant des besoins d'accessibilité. Lorsque vous définissez des états d'erreur, vous pouvez informer les services d'accessibilité de sa sémantique error et fournir des messages d'erreur développés.

Dans cet exemple, TalkBack lit les informations textuelles principales de l'erreur, suivies d'un message supplémentaire développé:

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

Composants de suivi de la progression

Pour les composants personnalisés qui suivent la progression, vous pouvez informer les utilisateurs de leurs changements de progression, y compris de la valeur de progression actuelle, de sa plage et de la taille de l'étape. Vous pouvez le faire avec la sémantique progressBarRangeInfo. Cela garantit que les services d'accessibilité sont informés des changements de progression et peuvent mettre à jour les utilisateurs en conséquence. Les différentes technologies d'assistance peuvent également avoir des moyens uniques d'indiquer une progression croissante et décroissante.

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

Informations sur la liste et les éléments

Dans les listes et les grilles personnalisées contenant de nombreux éléments, il peut être utile que les services d'accessibilité reçoivent également des informations plus détaillées, comme le nombre total d'éléments et d'indices.

En utilisant les sémantiques collectionInfo et collectionItemInfo sur la liste et les éléments respectivement, dans cette longue liste, les services d'accessibilité peuvent informer les utilisateurs de l'indice de l'élément par rapport à la collection totale, en plus des informations sémantiques textuelles:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

Description de l'état

Un composable peut définir une stateDescription pour la sémantique utilisée par le framework Android afin de lire l'état dans lequel se trouve le composable. Par exemple, un composable activable peut être à l'état "coché" ou "décoché". Dans certains cas, vous pouvez remplacer les étiquettes de description d'état par défaut utilisées 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() }
            )
    ) {
        /* ... */
    }
}

Actions personnalisées

Les actions personnalisées peuvent être utilisées pour des gestes tactiles plus complexes, comme balayer l'écran pour ignorer ou faire glisser et déposer, car ils peuvent être difficiles à utiliser pour les utilisateurs ayant des troubles moteurs ou d'autres handicaps.

Pour rendre le geste de balayage pour ignorer plus accessible, vous pouvez l'associer à une action personnalisée en y transmettant l'action et le libellé d'invalidation:

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

Un service d'accessibilité tel que TalkBack met ensuite en surbrillance le composant et indique que d'autres actions sont disponibles dans son menu, ce qui représente l'action de balayage pour ignorer:

Visualisation du menu d'actions TalkBack
Figure 4. Visualisation du menu d'action TalkBack.

Les longues listes d'éléments pour lesquels des actions personnalisées sont disponibles sont un autre cas d'utilisation des actions personnalisées, car il peut être fastidieux pour les utilisateurs d'itérer sur chaque action pour chaque élément individuellement:

=Visualisation de la navigation Switch Access à l'écran
Figure 5 : Visualisation de la navigation Switch Access à l'écran.

Pour améliorer l'expérience de navigation, ce qui est particulièrement utile pour les technologies d'assistance basées sur les interactions telles que Switch Access ou Voice Access, vous pouvez utiliser des actions personnalisées sur le conteneur pour déplacer les actions hors de la traversée individuelle et vers un menu d'actions distinct:

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

Dans ce cas, veillez à effacer manuellement la sémantique des enfants d'origine avec le modificateur clearAndSetSemantics, car vous les déplacez vers des actions personnalisées.

Prenons l'exemple de Switch Access. Son menu s'ouvre lorsque vous sélectionnez le conteneur et affiche les actions imbriquées disponibles:

Mise en surbrillance de l'élément de liste d'articles dans Switch Access
Figure 6 : Mise en surbrillance de l'élément de liste d'articles dans Switch Access.
Visualisation du menu d'actions de Switch Access.
Figure 7 Visualisation du menu d'actions de Switch Access.

Arborescence sémantique

Une composition décrit l'UI de votre application et est générée par l'exécution des composables. La composition est une arborescence composée des composables qui décrivent votre interface utilisateur.

À côté de la composition se trouve une arborescence parallèle, appelée arborescence sémantique. Cette arborescence décrit votre UI d'une manière alternative compréhensible pour les services d'accessibilité et pour le framework de test. Les services d'accessibilité l'utilisent pour décrire l'application aux utilisateurs ayant des besoins spécifiques. Le framework de test utilise l'arborescence pour interagir avec votre application et effectuer des assertions à son sujet. L'arborescence sémantique ne contient pas d'informations sur la façon de dessiner vos composables, mais sur leur signification sémantique.

Hiérarchie classique de l'UI avec son arborescence sémantique
Figure 8 : Hiérarchie classique de l'UI avec son arborescence sémantique.

Si votre application inclut des composables et des modificateurs tirés des bibliothèques Foundation et Material de Compose, l'arborescence sémantique est remplie et générée automatiquement. Cependant, lorsque vous ajoutez des composables personnalisés de bas niveau, vous devez fournir la sémantique manuellement. Votre arborescence pourrait ne pas refléter correctement ou complètement la signification des éléments affichés. Dans ce cas, vous pouvez adapter l'arborescence.

Prenons l'exemple de ce composable d'agenda personnalisé :

Composable d'agenda personnalisé avec des éléments "jour" sélectionnables
Figure 9. Composable d'agenda personnalisé avec des éléments "jour" sélectionnables

Dans cet exemple, l'intégralité de l'agenda est implémentée en tant que composable unique de bas niveau, en utilisant le composable Layout et en dessinant directement sur Canvas. Sans action supplémentaire de votre part, les services d'accessibilité ne recevront pas suffisamment d'informations sur le contenu du composable et la sélection de l'utilisateur dans l'agenda. Par exemple, si un utilisateur clique sur le jour "17", le framework d'accessibilité reçoit uniquement les informations de description pour la commande d'agenda entière. Dans ce cas, le service d'accessibilité TalkBack annonce "Agenda" ou, un peu mieux, "Agenda d'avril", et l'utilisateur se demande quel jour a été sélectionné. Pour rendre ce composable plus accessible, vous devez ajouter manuellement des informations sémantiques.

Arborescence fusionnée et non fusionnée

Comme mentionné précédemment, chaque composable de l'arborescence de l'UI peut avoir des propriétés sémantiques définies sur zéro ou plus. Lorsqu'aucune propriété sémantique n'est définie pour un composable, il n'est pas inclus dans l'arborescence sémantique. Ainsi, l'arborescence sémantique ne contient que les nœuds qui ont une incidence sémantique. Cependant, pour indiquer la signification correcte des informations affichées à l'écran, il est parfois utile de fusionner certaines sous-arborescences de nœuds et de les traiter comme un seul et même élément. De cette façon, vous pouvez traiter un ensemble de nœuds comme un tout, au lieu de traiter chaque nœud descendant individuellement. En règle générale, chaque nœud de cette arborescence représente un élément sélectionnable lorsque vous utilisez des services d'accessibilité.

Button est un exemple de composable de ce type. Vous pouvez considérer un bouton comme un élément unique, même s'il peut contenir plusieurs nœuds enfants:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Dans l'arborescence sémantique, les propriétés des descendants du bouton sont fusionnées et le bouton est présenté comme un nœud feuille unique dans l'arborescence:

Représentation sémantique d'une seule feuille fusionnée
Figure 10. Représentation sémantique d'une seule feuille fusionnée.

Les composables et les modificateurs peuvent indiquer qu'ils souhaitent fusionner les propriétés sémantiques de leurs descendants en appelant Modifier.semantics (mergeDescendants = true) {}. Si vous définissez cette propriété sur true, vous indiquez que les propriétés sémantiques doivent être fusionnées. Dans l'exemple Button, le composable Button utilise le modificateur clickable en interne qui inclut ce modificateur semantics. Par conséquent, les nœuds descendants du bouton sont fusionnés. Lisez la documentation sur l'accessibilité pour savoir quand modifier le comportement de fusion dans votre composable.

Plusieurs modificateurs et composables des bibliothèques Foundation et Material Compose disposent de cette propriété. Par exemple, les modificateurs clickable et toggleable fusionnent automatiquement leurs descendants. De plus, le composable ListItem fusionnera ses descendants.

Inspecter l'arborescence

L'arborescence sémantique est en fait composée de deux arbres différents. Il existe une arborescence sémantique fusionnée qui fusionne les nœuds descendants lorsque mergeDescendants est défini sur true. Il existe également une arborescence sémantique non fusionnée, qui n'applique pas la fusion, mais conserve chaque nœud intact. Les services d'accessibilité utilisent l'arborescence non fusionnée et appliquent leurs propres algorithmes de fusion, en tenant compte de la propriété mergeDescendants. Le framework de test utilise l'arborescence fusionnée par défaut.

Vous pouvez inspecter ces deux arborescences avec la méthode printToLog(). Par défaut, et comme dans les exemples précédents, l'arborescence fusionnée est consignée. Pour imprimer l'arborescence non fusionnée à la place, définissez le paramètre useUnmergedTree de l'outil de mise en correspondance onRoot() sur true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

L'outil d'inspection de la mise en page vous permet d'afficher l'arborescence sémantique fusionnée et l'arborescence sémantique non fusionnée en sélectionnant celle que vous préférez dans le filtre de vue:

Options d'affichage de l'outil d'inspection de la mise en page permettant d'afficher à la fois l'arborescence sémantique fusionnée et l'arborescence sémantique non fusionnée
Figure 11 Options d'affichage de l'outil d'inspection de la mise en page permettant d'afficher à la fois l'arborescence sémantique fusionnée et l'arborescence sémantique non fusionnée.

Pour chaque nœud de votre arborescence, l'outil d'inspection de mise en page affiche la sémantique fusionnée et la sémantique définie sur ce nœud dans le panneau des propriétés :

Propriétés sémantiques fusionnées et définies
Figure 12 Propriétés sémantiques fusionnées et définies.

Par défaut, les outils de mise en correspondance du framework de test utilisent l'arborescence sémantique fusionnée. C'est la raison pour laquelle vous pouvez interagir avec un Button en faisant correspondre le texte qu'il contient:

composeTestRule.onNodeWithText("Like").performClick()

Ignorez ce comportement en définissant le paramètre useUnmergedTree des outils de mise en correspondance sur true, comme avec l'outil de mise en correspondance onRoot.

Adapter l'arborescence

Comme indiqué précédemment, vous pouvez remplacer ou effacer certaines propriétés sémantiques, ou modifier le comportement de fusion de l'arborescence. C'est particulièrement utile lorsque vous créez vos propres composants personnalisés. Si vous ne définissez pas les bonnes propriétés et le bon comportement de fusion, votre application risque de ne pas être accessible et les tests pourraient produire un résultat différent. Pour en savoir plus sur les tests, consultez le guide de test.