Semantica in Compose

Una composizione descrive l'UI della tua app ed è prodotta eseguendo componibili. La composizione è una struttura ad albero composta dagli elementi componibili che descrivono la tua UI.

Accanto alla composizione esiste un albero parallelo chiamato albero della semantica. Questo albero descrive la tua UI in un modo alternativo comprensibile per i servizi di accessibilità e per il framework Testing. I servizi di accessibilità utilizzano la struttura per descrivere l'app agli utenti con un'esigenza specifica. Il framework di test utilizza la struttura ad albero per interagire con l'app e fare affermazioni al riguardo. L'albero semantico non contiene le informazioni su come disegnare i componibili, ma contiene informazioni sul significato semantico degli elementi componibili.

Una tipica gerarchia dell'interfaccia utente e la sua semantica
Figura 1. Una tipica gerarchia dell'interfaccia utente e del suo albero della semantica.

Se la tua app è composta da elementi componibili e modificatori della libreria di base e dei materiali di Compose, l'albero della semantica viene compilato e generato automaticamente. Tuttavia, quando aggiungi elementi componibili personalizzati di basso livello, devi fornerne manualmente la semantica. Potrebbero verificarsi anche situazioni in cui la struttura ad albero non rappresenta correttamente o in modo completo il significato degli elementi sullo schermo; in tal caso puoi adattare la struttura.

Considera ad esempio questo calendario componibile personalizzato:

Un calendario personalizzato componibile con elementi giornalieri selezionabili
Figura 2. Un calendario personalizzato componibile con elementi giornalieri selezionabili.

In questo esempio, l'intero calendario viene implementato come un unico componibile di basso livello, utilizzando l'elemento componibile Layout e disegnandolo direttamente in Canvas. Se non fai altro, i servizi di accessibilità non riceveranno informazioni sufficienti sui contenuti del componibile e sulla selezione dell'utente nel calendario. Ad esempio, se un utente fa clic sul giorno che contiene 17, il framework per l'accessibilità riceve solo le informazioni di descrizione per l'intero controllo del calendario. In questo caso, il servizio di accessibilità TalkBack riporterebbe "Calendar" o, solo leggermente meglio, "Calendario di aprile" e l'utente verrebbe lasciato a chiedersi che giorno sia stato selezionato. Per rendere più accessibile questo componibile, devi aggiungere manualmente le informazioni semantiche.

Proprietà semantiche

Tutti i nodi nell'albero dell'interfaccia utente con un significato semantico hanno un nodo parallelo nell'albero semantico. Il nodo nell'albero semantico contiene le proprietà che trasmettono il significato del componibile corrispondente. Ad esempio, l'elemento componibile Text contiene una proprietà semantica text, perché questo è il significato dell'elemento componibile. Un Icon contiene una proprietà contentDescription (se impostata dallo sviluppatore) che comunica nel testo il significato di Icon. I componenti e modificatori creati sulla base della libreria di base di Compose già impostano le proprietà pertinenti. Se vuoi, imposta o sostituisci le proprietà autonomamente con i modificatori semantics e clearAndSetSemantics. Ad esempio, aggiungi azioni di accessibilità personalizzate a un nodo, fornisci una descrizione dello stato alternativa per un elemento attivabile oppure indica che un determinato testo componibile deve essere considerato come intestazione.

Per visualizzare l'albero della semantica, utilizza lo strumento Controllo layout oppure il metodo printToLog() all'interno dei test. Viene visualizzato l'albero della semantica attuale in 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")
    }
}

L'output di questo test sarebbe:

    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]

Considera come le proprietà semantiche trasmettono il significato di un componibile. Considera un Switch. Ecco come appare all'utente:

Figura 3. un interruttore nello stato "On" e "Off".

Per descrivere il significato di questo elemento, potresti dire: "Questo è un Switch, che è un elemento attivabile nello stato "On". Puoi fare clic per interagire con essa."

Questo è esattamente lo scopo per cui vengono utilizzate le proprietà semantiche. Il nodo della semantica di questo elemento Switch contiene le seguenti proprietà, come visualizzate con Layout Inspector:

Layout Inspector che mostra le proprietà semantiche di un componibile Switch
Figura 4. Layout Inspector che mostra le proprietà semantiche di un componibile Switch.

Role indica il tipo di elemento. L'elemento StateDescription descrive come fare riferimento allo stato "On". Per impostazione predefinita, si tratta di una versione localizzata della parola "On", ma può essere resa più specifica (ad esempio, "Enabled") in base al contesto. ToggleableState indica lo stato attuale dello Switch. La proprietà OnClick fa riferimento al metodo utilizzato per interagire con questo elemento. Per un elenco completo delle proprietà semantiche, consulta l'oggetto SemanticsProperties. Per un elenco completo delle possibili azioni di accessibilità, controlla l'oggetto SemanticsActions.

Tenere traccia delle proprietà semantiche di ogni componibile nell'app sblocca molte potenti possibilità. Alcuni esempi:

  • TalkBack utilizza le proprietà per leggere ad alta voce ciò che viene mostrato sullo schermo e consente all'utente di interagire senza problemi con lo schermo. Per lo switch componibile, TalkBack potrebbe dire: "On; Switch; tocca due volte per attivare/disattivare". L'utente può toccare due volte lo schermo per disattivare l'opzione.
  • Il framework di test utilizza le proprietà per trovare i nodi, interagirvi e creare asserzioni. Un test di esempio per Switch potrebbe essere:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Albero della semantica unito e non unito

Come accennato prima, ogni componibile nell'albero dell'interfaccia utente potrebbe avere zero o più proprietà semantiche impostate. Quando un componibile non ha proprietà semantiche impostate, non viene incluso nell'albero della semantica. In questo modo l'albero semantico contiene solo i nodi che contengono effettivamente un significato semantico. Tuttavia, a volte, per trasmettere il significato corretto di ciò che viene visualizzato sullo schermo, è anche utile unire alcuni sottoalberi di nodi e trattarli come uno solo. In questo modo puoi ragionare su un insieme di nodi nel suo complesso, invece di occuparti di ogni nodo discendente singolarmente. Come regola generale, ogni nodo in questo albero rappresenta un elemento attivabile quando si utilizzano i servizi di accessibilità.

Un esempio di questo componibile è Button. Puoi ragionare su un pulsante come singolo elemento, anche se può contenere più nodi secondari:

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

Nell'albero semantico, le proprietà dei discendenti del pulsante vengono unite e il pulsante viene presentato come un singolo nodo foglia nell'albero:

Rappresentazione della semantica di una singola foglia unita
Figura 5. Rappresentazione della semantica di una singola foglia.

I componibili e i modificatori possono indicare che vogliono unire le proprietà semantiche dei loro discendenti chiamando Modifier.semantics (mergeDescendants = true) {}. L'impostazione di questa proprietà su true indica che le proprietà semantiche devono essere unite. Nell'esempio Button, il componibile Button utilizza internamente il modificatore clickable che include questo modificatore semantics. Di conseguenza, i nodi discendenti del pulsante vengono uniti. Leggi la documentazione sull'accessibilità per scoprire di più su quando dovresti modificare il comportamento di unione nel tuo componibile.

Questa proprietà è impostata per diversi modificatori e componibili nelle librerie Foundation e Material Compose. Ad esempio, i modificatori clickable e toggleable uniranno automaticamente i rispettivi discendenti. Inoltre, l'elemento componibile ListItem unisce i suoi discendenti.

Ispeziona gli alberi

In realtà, l'albero della semantica è costituito da due alberi diversi. Esiste un albero della semantica unito, che unisce i nodi discendenti quando mergeDescendants è impostato su true. C'è anche un albero della semantica non unito, che non applica l'unione, ma mantiene ogni nodo intatto. I servizi di accessibilità utilizzano la struttura ad albero non unita e applicano i propri algoritmi di unione, prendendo in considerazione la proprietà mergeDescendants. Per impostazione predefinita, il framework di test utilizza l'albero unito.

Puoi esaminare entrambi gli alberi con il metodo printToLog(). Per impostazione predefinita, e come negli esempi precedenti, l'albero unito viene registrato. Per stampare invece l'albero non unito, imposta il parametro useUnmergedTree del matcher onRoot() su true:

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

La finestra Controllo layout ti consente di visualizzare sia l'albero della semantica unita sia quello non unito, selezionando quello preferito nel filtro della vista:

Opzioni di visualizzazione di Layout Inspector, che consentono sia la visualizzazione dell'albero semantico unito e non unito
Figura 6. Opzioni di visualizzazione di Layout Inspector, che consentono sia la visualizzazione dell'albero della semantica unito a quello non unito.

Per ogni nodo nell'albero, Controllo layout mostra sia la semantica unita sia la semantica unita impostate su tale nodo nel riquadro delle proprietà:

Proprietà semantiche unite e impostate
Figura 7. Proprietà semantiche unite e impostate.

Per impostazione predefinita, i matcher nel framework di test utilizzano l'albero semantico unito. Ecco perché puoi interagire con Button facendo corrispondere il testo visualizzato al suo interno:

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

Esegui l'override di questo comportamento impostando il parametro useUnmergedTree dei matcher su true, come con il matcher onRoot.

Comportamento di unione

Quando un componibile indica che i suoi discendenti devono essere uniti, come avviene esattamente questa unione?

Ogni proprietà semantica ha una strategia di unione definita. Ad esempio, la proprietà ContentDescription aggiunge tutti i valori ContentDescription discendenti a un elenco. Verifica la strategia di unione di una proprietà semantics controllando la relativa implementazione di mergePolicy in SemanticsProperties.kt. Le proprietà possono assumere il valore principale o secondario, unire i valori in un elenco o in una stringa, non consentire alcuna unione e generare un'eccezione o qualsiasi altra strategia di unione personalizzata.

Una nota importante è che i discendenti che hanno impostato a loro volta mergeDescendants = true non sono inclusi nell'unione. Vedi un esempio:

Voce dell'elenco con immagine, del testo e un'icona dei preferiti
Figura 8. Voce dell'elenco con immagine, del testo e un'icona dei preferiti.

Ecco una voce di elenco cliccabile. Quando l'utente preme sulla riga, l'app va alla pagina dei dettagli dell'articolo, dove può leggerlo. All'interno dell'elemento dell'elenco è presente un pulsante per aggiungere ai preferiti l'articolo, che forma un elemento cliccabile nidificato, in modo che il pulsante venga visualizzato separatamente nell'albero unito. Il resto dei contenuti della riga viene unito:

La struttura ad albero unito contiene più testi in un elenco all'interno del nodo Riga. La struttura ad albero non unita contiene nodi separati per ogni componibile di testo.
Figura 9. La struttura ad albero unito contiene più testi in un elenco all'interno del nodo Riga. L'albero non unito contiene nodi separati per ogni componibile di testo.

Adattare l'albero della semantica

Come accennato prima, puoi sostituire o cancellare determinate proprietà semantiche o modificare il comportamento di unione dell'albero. Ciò è particolarmente importante quando crei i tuoi componenti personalizzati. Se non imposti le proprietà e il comportamento di unione corretti, la tua app potrebbe non essere accessibile e i test potrebbero comportarsi in modo diverso da quanto previsto. Per ulteriori informazioni su alcuni casi d'uso comuni in cui è necessario adattare l'albero della semantica, consulta la documentazione sull'accessibilità. Per ulteriori informazioni sui test, consulta la guida ai test.

Risorse aggiuntive