Sémantique dans Compose

Une composition décrit l'interface utilisateur de votre application. Elle est générée par l'exécution de composables. La composition est une arborescence composée de 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é utilisent l'arborescence pour décrire l'application aux utilisateurs ayant un besoin spécifique. 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 manière de dessiner vos composables, mais sur leur signification sémantique.

Hiérarchie classique de l'UI et son arborescence sémantique
Figure 1 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. Toutefois, lorsque vous ajoutez des composables personnalisés de bas niveau, vous devez fournir manuellement sa sémantique. 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 de jour sélectionnables
Figure 2 Composable d'agenda personnalisé avec des éléments de 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. Si vous ne faites rien d'autre, les services d'accessibilité ne recevront pas suffisamment d'informations sur le contenu du composable et sur 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 annoncera "Agenda" ou, un peu mieux, "Agenda d'avril", et l'utilisateur se demandera quel jour a été sélectionné. Pour rendre ce composable plus accessible, vous devez ajouter manuellement des informations sémantiques.

Propriétés sémantiques

Tous les nœuds de l'arborescence d'UI ayant une incidence sémantique disposent d'un nœud parallèle dans l'arborescence sémantique. Le nœud de l'arborescence sémantique contient les propriétés qui ont 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 Icon. Les composables et les modificateurs créés sur la bibliothèque de base Compose définissent déjà les propriétés pertinentes pour vous. Vous pouvez également définir ou remplacer les propriétés vous-même avec les modificateurs semantics et clearAndSetSemantics. Par exemple, ajoutez des actions d'accessibilité personnalisées à un nœud, fournissez une autre description d'état pour un élément activable ou indiquez qu'un composable de texte spécifique doit être considéré comme un titre.

Pour visualiser l'arborescence sémantique, utilisez l'outil d'inspection de la mise en page ou la méthode printToLog() dans les tests. L'arborescence sémantique actuelle s'affiche dans Logcat.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

Le résultat est le suivant :

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Réfléchissez à la manière dont les propriétés sémantiques transmettent la signification d'un composable. Envisagez d'utiliser un Switch. Voici comment il se présente :

Figure 3. Un interrupteur à l'état "Activé" ou "Désactivé".

Pour décrire la signification de cet élément, vous pouvez dire ce qui suit: "Il s'agit d'un bouton bascule, qui est un élément activable à l'état activé. Vous pouvez cliquer dessus pour interagir."

C'est précisément ce à quoi servent les propriétés sémantiques. Le nœud sémantique de cet élément Switch 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 Switch (Bouton bascule)
Figure 4. Outil d'inspection de la mise en page affichant les propriétés sémantiques d'un composable Switch (Bouton bascule)

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 cela peut être plus spécifique (par exemple, "Enabled") en fonction du contexte. ToggleableState correspond à l'état actuel du commutateur. La propriété OnClick fait référence à la méthode utilisée pour interagir avec cet élément. 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.

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

  • TalkBack utilise les propriétés pour lire à voix haute ce qui est affiché à l'écran et permettre à l'utilisateur d'interagir en douceur avec celui-ci. Pour le composable "Switch" (Bouton bascule), TalkBack peut dire: "On; Switch; double tap to switch". 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. Voici un exemple de test pour le bouton bascule :
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Arborescence sémantique 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'une propriété sémantique n'est pas 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 dans son ensemble, 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 ce composable. Vous pouvez considérer un bouton comme un seul élément, 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 à feuille unique fusionnée
Figure 5. Représentation sémantique à feuille unique 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 dans quels cas vous devez 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 les arborescences

L'arborescence sémantique est en fait constitué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 tous les nœuds intacts. 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 à la fois 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, autorisant à la fois l'affichage de l'arborescence sémantique fusionnée et non fusionnée
Figure 6 Options d'affichage de l'outil d'inspection de la mise en page, autorisant à la fois l'affichage de l'arborescence sémantique fusionnée et 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 7 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 pourquoi vous pouvez interagir avec un Button en faisant correspondre le texte qu'il contient:

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

Remplacez ce comportement en définissant le paramètre useUnmergedTree des outils de mise en correspondance sur true, comme pour onRoot.

Comportement de la fusion

Lorsqu'un composable indique que ses descendants doivent être fusionnés, comment cette fusion se produit-elle exactement ?

Chaque propriété sémantique a une stratégie de fusion définie. Par exemple, la propriété ContentDescription ajoute toutes les valeurs descendantes ContentDescription à une liste. Vérifiez la stratégie de fusion d'une propriété sémantique en vérifiant son implémentation de mergePolicy dans SemanticsProperties.kt. Les propriétés peuvent prendre en compte la valeur parente ou enfant, fusionner les valeurs dans une liste ou une chaîne, ne pas autoriser la fusion du tout et générer une exception à la place, ou toute autre stratégie de fusion personnalisée.

Il est important de noter que les descendants qui ont eux-mêmes défini mergeDescendants = true ne sont pas inclus dans la fusion. Prenons un exemple:

Élément de liste avec une image, du texte et une icône de favori
Figure 8 Élément de liste avec une image, du texte et une icône de favori.

Voici un élément de liste cliquable. Lorsque l'utilisateur appuie sur la ligne, il accède à la page de détails de l'article et peut le lire. Dans l'élément de liste, un bouton permet d'ajouter l'article aux favoris. Il constitue un élément cliquable imbriqué. Le bouton s'affiche donc séparément dans l'arborescence fusionnée. Le reste du contenu de la ligne est fusionné :

L'arborescence fusionnée contient plusieurs textes d'une liste au sein du nœud "Ligne". L'arborescence non fusionnée contient des nœuds distincts pour chaque composable Text.
Figure 9 L'arborescence fusionnée contient plusieurs textes d'une liste au sein du nœud "Ligne". L'arborescence non fusionnée contient des nœuds distincts pour chaque composable Text.

Adapter l'arborescence sémantique

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 important lorsque vous créez vos propres composants personnalisés. Si vous ne définissez pas les propriétés et le comportement de fusion appropriés, votre application risque de ne pas être accessible et les tests peuvent se comporter différemment de ce à quoi vous vous attendiez. Pour en savoir plus sur certains cas d'utilisation courants dans lesquels vous devez adapter l'arborescence sémantique, consultez la documentation sur l'accessibilité. Si vous souhaitez en savoir plus sur les tests, consultez le guide des tests.

Ressources supplémentaires