Fusion et nettoyage

Lorsque les services d'accessibilité parcourent les éléments à l'écran, il est important que ces éléments soient regroupés, séparés ou même masqués avec la précision appropriée. Lorsque chaque composable de bas niveau à l'écran est mis en surbrillance indépendamment, les utilisateurs doivent beaucoup interagir pour se déplacer dans l'écran. Si les éléments sont fusionnés de manière excessive, les utilisateurs pourraient ne pas comprendre quels éléments vont ensemble. Si des éléments de l'écran sont purement décoratifs, ils peuvent être masqués des services d'accessibilité. Dans ce cas, vous pouvez utiliser les API Compose pour fusionner, effacer et masquer les sémantiques.

Sémantique de fusion

Lorsque vous appliquez un modificateur clickable à un composable parent, Compose fusionne automatiquement tous les éléments enfants sous celui-ci. Pour comprendre comment les composants Compose Material et Foundation interactifs utilisent des stratégies de fusion par défaut, consultez la section Éléments interactifs.

Il est courant qu'un composant se compose de plusieurs composables. Ces composables peuvent former un groupe logique et chacun peut contenir des informations importantes, mais vous souhaitez peut-être que les services d'accessibilité les considèrent comme un seul élément.

Par exemple, imaginez un composable qui affiche l'avatar d'un utilisateur, son nom et d'autres informations supplémentaires:

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

Vous pouvez demander à Compose de fusionner ces éléments à l'aide du paramètre mergeDescendants dans le modificateur de sémantique. De cette façon, les services d'accessibilité traitent le composant comme une entité, 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 fusionnant son contenu:

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

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. Vous pouvez vérifier la stratégie de fusion d'une propriété sémantique en regardant son implémentation mergePolicy dans SemanticsProperties.kt. Les propriétés peuvent prendre la valeur parent 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.

Dans d'autres scénarios, vous vous attendez à ce que les sémantiques enfants soient fusionnées dans une sémantique parente, mais cela ne se produit pas. Dans l'exemple suivant, nous avons un élément parent de liste clickable avec des éléments enfants, et nous nous attendons à ce que le parent les fusionne tous:

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

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

Lorsque l'utilisateur appuie sur l'élément clickable Row, l'article s'ouvre. En interne, il existe un BookmarkButton permettant d'ajouter l'article aux favoris. Ce bouton imbriqué s'affiche comme non fusionné, tandis que le reste du contenu enfant 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 4. L'arborescence fusionnée contient plusieurs textes d'une liste au sein du nœud Row. L'arborescence non fusionnée contient des nœuds distincts pour chaque composable Text.

Par conception, certains composables ne sont pas automatiquement fusionnés sous un parent. Un parent ne peut pas fusionner ses enfants lorsque les enfants fusionnent également, soit en définissant mergeDescendants = true explicitement, soit en étant des composants qui se fusionnent eux-mêmes, comme des boutons ou des éléments cliquables. Savoir comment certaines API fusionnent ou défient la fusion peut vous aider à déboguer certains comportements potentiellement inattendus.

Utilisez la fusion lorsque les éléments enfants constituent un groupe logique et pertinent sous leur parent. Toutefois, si les enfants imbriqués doivent être ajustés manuellement ou si leur propre sémantique doit être supprimée, d'autres API peuvent mieux répondre à vos besoins (par exemple, clearAndSetSemantics).

Effacer et définir la sémantique

Si les informations sémantiques doivent être complètement effacées ou écrasées, utilisez l'API clearAndSetSemantics.

Lorsqu'un composant doit effacer sa propre sémantique et celle de ses descendants, utilisez cette API avec un lambda vide. Lorsque sa sémantique doit être écrasée, incluez votre nouveau contenu dans le lambda.

Notez que lors de la suppression avec un lambda vide, les sémantiques effacées ne sont envoyées à aucun consommateur qui utilise ces informations, comme l'accessibilité, le remplissage automatique ou les tests. Lorsque vous remplacez du contenu par clearAndSetSemantics{/*semantic information*/}, la nouvelle sémantique remplace toutes les sémantiques précédentes de l'élément et de ses descendants.

Voici un exemple de composant de bouton d'activation/de désactivation personnalisé, représenté par une ligne interactive avec une icône et du texte:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

Bien que l'icône et le texte contiennent des informations sémantiques, ils n'indiquent pas ensemble que ce composant peut être activé/désactivé. La fusion n'est pas suffisante, car vous devez fournir des informations supplémentaires sur le composant.

Étant donné que l'extrait de code ci-dessus crée un composant de bouton d'activation/de désactivation personnalisé, vous devez ajouter la fonctionnalité de bouton d'activation/de désactivation, ainsi que les sémantiques stateDescription, toggleableState et role. De cette façon, l'état du composant et l'action associée sont disponibles. Par exemple, TalkBack annonce "Appuyer deux fois pour activer/désactiver" au lieu de "Appuyer deux fois pour activer".

En effaçant les sémantiques d'origine et en définissant de nouvelles sémantiques plus descriptives, les services d'accessibilité peuvent désormais voir qu'il s'agit d'un composant à activer/désactiver qui peut changer d'état.

Lorsque vous utilisez clearAndSetSemantics, tenez compte des points suivants:

  • Étant donné que les services ne reçoivent aucune information lorsque cette API est définie, il est préférable de l'utiliser avec parcimonie.
    • Les informations sémantiques peuvent être utilisées par les agents d'IA et les services similaires pour comprendre l'écran. Elles ne doivent donc être effacées que si nécessaire.
  • Des sémantiques personnalisées peuvent être définies dans le lambda de l'API.
  • L'ordre des modificateurs est important : cette API efface toutes les sémantiques qui se trouvent après l'endroit où elle est appliquée, quelles que soient les autres stratégies de fusion.

Sémantique masquée

Dans certains cas, les éléments n'ont pas besoin d'être envoyés aux services d'accessibilité. Leurs informations supplémentaires sont peut-être redondantes pour l'accessibilité, ou elles sont purement décoratives et non interactives. Dans ce cas, vous pouvez masquer des éléments avec l'API hideFromAccessibility.

Les exemples suivants présentent des composants qui peuvent nécessiter d'être masqués: un filigrane redondant qui s'étend sur un composant et un caractère utilisé pour séparer de manière décorative les informations.

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

L'utilisation de hideFromAccessibility ici garantit que le filigrane et la décoration sont masqués des services d'accessibilité, mais conservent leur sémantique pour d'autres cas d'utilisation, comme les tests.

Répartition des cas d'utilisation

Vous trouverez ci-dessous un résumé des cas d'utilisation pour comprendre comment différencier clairement les API précédentes:

  • Lorsque le contenu n'est pas destiné à être utilisé par les services d'accessibilité :
    • Utilisez hideFromAccessibility lorsque le contenu est éventuellement décoratif ou redondant, mais doit tout de même être testé.
    • Utilisez clearAndSetSemantics{} avec un lambda vide lorsque la sémantique des parents et des enfants doit être effacée pour tous les services.
    • Utilisez clearAndSetSemantics{/*content*/} avec du contenu dans le lambda lorsque la sémantique d'un composant doit être définie manuellement.
  • Lorsque le contenu doit être traité comme une entité et que toutes les informations de ses enfants doivent être complètes :
    • Utilisez des descendants sémantiques de fusion.
Tableau des cas d'utilisation différenciés des API.
Figure 5 Tableau des cas d'utilisation différenciés des API.