Semantica

Oltre alle informazioni principali contenute in un composable, come una stringa di testo di un composable Text, può essere utile avere ulteriori informazioni supplementari sugli elementi UI.

Le informazioni sul significato e sul ruolo di un componente in Compose sono chiamate semantica, che è un modo per fornire un contesto aggiuntivo sui componibili a servizi come accessibilità, compilazione automatica e test. Ad esempio, l'icona di una fotocamera potrebbe essere visivamente solo un'immagine, ma il significato semantico potrebbe essere "Scatta una foto".

Combinando la semantica appropriata con le API Compose appropriate, fornisci ai servizi di accessibilità quante più informazioni possibili sul tuo componente, che poi decidono come rappresentarlo all'utente.

Le API Material e Compose UI and Foundation sono dotate di semantica integrata che segue il loro ruolo e la loro funzione specifici, ma puoi anche modificare questa semantica per le API esistenti o impostarne una nuova per i componenti personalizzati, in base ai tuoi requisiti specifici.

Proprietà semantiche

Le proprietà semantiche trasmettono il significato del composable corrispondente. Ad esempio, il componibile Text contiene una proprietà semantica text, perché questo è il significato di quel componibile. Un Icon contiene una proprietà contentDescription (se impostata dallo sviluppatore) che spiega in formato testo il significato dell'icona.

Considera il modo in cui le proprietà di semantica trasmettono il significato di un composable. Prendi in considerazione un Switch. Ecco come appare all'utente:

Figura 1. Un Switch nello stato "On" e "Off".

Per descrivere il significato di questo elemento, potresti dire: "Questo è un interruttore, ovvero un elemento attivabile/disattivabile nello stato 'On'. Puoi fare clic per interagire."

Questo è esattamente lo scopo delle proprietà di semantica. Il nodo di semantica di questo elemento Switch contiene le seguenti proprietà, visualizzate con Layout Inspector:

Layout Inspector che mostra le proprietà di Semantics di un composable Switch
Figura 2. Layout Inspector che mostra le proprietà di Semantics di un componibile Switch.

Role indica il tipo di elemento. StateDescription descrive come deve essere indicato lo stato "On". Per impostazione predefinita, si tratta di una versione localizzata della parola "On", ma può essere resa più specifica (ad esempio "Attivato") in base al contesto. ToggleableState è lo stato attuale dell'interruttore. La proprietà OnClick fa riferimento al metodo utilizzato per interagire con questo elemento.

Tenere traccia delle proprietà semantiche di ogni composable nella tua app sblocca molte potenti possibilità:

  • I servizi di accessibilità utilizzano le proprietà per rappresentare la UI mostrata sullo schermo e consentire agli utenti di interagire con questa. Per l'elemento componibile Switch, TalkBack potrebbe annunciare: "On; Switch; tocca due volte per attivare/disattivare". L'utente può toccare due volte lo schermo per disattivare l'interruttore.
  • Il framework di test utilizza le proprietà per trovare i nodi, interagire con loro ed eseguire asserzioni:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Gli elementi componibili e i modificatori basati sulla libreria foundation di Compose impostano già le proprietà pertinenti per impostazione predefinita. Se vuoi, puoi modificare queste proprietà manualmente per migliorare il supporto dell'accessibilità per casi d'uso specifici o modificare la strategia di unione o cancellazione dei composabili.

Per segnalare il tipo di contenuto specifico del componente ai servizi di accessibilità, puoi applicare una serie di semantiche diverse. Queste aggiunte supporteranno le informazioni semantiche principali esistenti e aiuteranno i servizi di accessibilità a perfezionare il modo in cui il componente viene rappresentato, annunciato o interagito.

Per un elenco completo delle proprietà di semantica, consulta l'oggetto SemanticsProperties. Per un elenco completo delle possibili azioni di accessibilità, consulta l'oggetto SemanticsActions.

Intestazioni

Le app spesso contengono schermate con contenuti avanzati, come articoli lunghi o pagine di notizie, che di solito sono suddivisi in diverse sottosezioni con intestazioni:

Un blog post con il testo dell'articolo in un contenitore scorrevole.
Figura 3. Un blog post con il testo dell'articolo in un contenitore scorrevole.

Gli utenti con esigenze di accessibilità possono avere difficoltà a navigare facilmente in una schermata di questo tipo. Per migliorare l'esperienza di navigazione, alcuni servizi di accessibilità consentono una navigazione più semplice direttamente tra sezioni o intestazioni. Per attivare questa opzione, indica che il tuo componente è un heading definendo la relativa proprietà di semantica:

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

Avvisi e popup

Se il componente è un avviso o un popup, ad esempio un Snackbar, potresti voler segnalare ai servizi di accessibilità che una nuova struttura o gli aggiornamenti ai contenuti possono essere comunicati agli utenti.

I componenti simili ad avvisi possono essere contrassegnati con la proprietà di semantica liveRegion. In questo modo, i servizi di accessibilità possono informare automaticamente l'utente delle modifiche apportate a questo componente o ai relativi elementi secondari:

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

Nella maggior parte dei casi, devi utilizzare liveRegionMode.Polite quando l'attenzione degli utenti deve essere attirata solo brevemente da avvisi o contenuti importanti che cambiano sullo schermo.

Dovresti utilizzare liveRegion.Assertive con moderazione per evitare feedback invasivi. Deve essere utilizzato in situazioni in cui è fondamentale che gli utenti siano informati di contenuti sensibili al fattore tempo:

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

Le aree dinamiche non devono essere utilizzate per contenuti che si aggiornano di frequente, come i timer conto alla rovescia, per evitare di sovraccaricare gli utenti con un feedback costante.

Componenti simili a finestre

I componenti personalizzati simili a finestre, come ModalBottomSheet, richiedono indicatori aggiuntivi per distinguerli dai contenuti circostanti. A questo scopo, puoi utilizzare la semantica paneTitle, in modo che eventuali modifiche pertinenti alla finestra o al riquadro possano essere rappresentate in modo appropriato dai servizi di accessibilità, insieme alle informazioni semantiche principali:

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

Per riferimento, scopri come Material 3 utilizza paneTitle per i suoi componenti.

Componenti degli errori

Per altri tipi di contenuti, come i componenti simili a errori, potresti voler espandere le informazioni semantiche principali per gli utenti con esigenze di accessibilità. Quando definisci gli stati di errore, puoi informare i servizi di accessibilità della semantica di error e fornire messaggi di errore espansi.

In questo esempio, TalkBack legge le informazioni di testo dell'errore principale, seguite da messaggi aggiuntivi espansi:

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

Componenti del monitoraggio dei progressi

Per i componenti personalizzati che monitorano l'avanzamento, potresti voler informare gli utenti delle modifiche all'avanzamento, inclusi il valore di avanzamento corrente, l'intervallo e le dimensioni dei passaggi. Puoi farlo con la semantica di progressBarRangeInfo, che garantisce che i servizi di accessibilità siano consapevoli delle modifiche allo stato di avanzamento e possano aggiornare gli utenti di conseguenza. Tecnologie per la disabilità diverse potrebbero anche avere modi unici per suggerire l'aumento e la diminuzione della progressione.

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

Informazioni su elenchi ed elementi

In elenchi e griglie personalizzati con molti elementi, potrebbe essere utile per i servizi di accessibilità ricevere anche informazioni più dettagliate, come il numero totale di elementi e indici.

Utilizzando la semantica collectionInfo e collectionItemInfo rispettivamente nell'elenco e negli elementi, in questo lungo elenco, i servizi di accessibilità possono informare gli utenti dell'indice degli elementi in cui si trovano rispetto alla raccolta totale, oltre alle informazioni semantiche testuali:

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)
            }
        )
    }
}

Descrizione dello stato

Un componibile può definire un stateDescription per la semantica che il framework Android utilizza per leggere lo stato in cui si trova il componibile. Ad esempio, un composable attivabile/disattivabile può essere in stato "selezionato" o "non selezionato". In alcuni casi, potresti voler eseguire l'override delle etichette di descrizione dello stato predefinite utilizzate da Compose. Per farlo, specifica esplicitamente le etichette di descrizione dello stato prima di definire un composable come attivabile/disattivabile:

@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() }
            )
    ) {
        /* ... */
    }
}

Azioni personalizzate

Le azioni personalizzate possono essere utilizzate per gesti più complessi sul touchscreen, come lo scorrimento per ignorare o il trascinamento, in quanto possono essere difficili da eseguire per gli utenti con disabilità motorie o di altro tipo.

Per rendere più accessibile il gesto di scorrimento per chiudere, puoi collegarlo a un'azione personalizzata, passando l'azione di chiusura e l'etichetta:

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 servizio di accessibilità come TalkBack evidenzia quindi il componente e suggerisce che sono disponibili altre azioni nel relativo menu, che rappresenta l'azione di scorrimento per ignorare:

Visualizzazione del menu Azioni di TalkBack
Figura 4. Visualizzazione del menu Azioni di TalkBack.

Un altro caso d'uso delle azioni personalizzate sono gli elenchi lunghi con elementi che hanno più azioni disponibili, in quanto potrebbe essere noioso per gli utenti scorrere ogni azione per ogni singolo elemento singolarmente:

=Visualizzazione della navigazione di Switch Access sullo schermo
Figura 5. Visualizzazione della navigazione di Switch Access sullo schermo.

Per migliorare l'esperienza di navigazione, il che è particolarmente utile per le tecnologie per la disabilità basate sull'interazione come Switch Access o Voice Access, puoi utilizzare azioni personalizzate sul container per spostare le azioni fuori dall'attraversamento individuale e in un menu Azioni separato:

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,
    )
}

In questi casi, assicurati di cancellare manualmente la semantica originale degli elementi secondari con il modificatore clearAndSetSemantics, poiché li stai spostando in azioni personalizzate.

Prendendo come esempio Switch Access, il relativo menu si apre quando viene selezionato il contenitore ed elenca le azioni nidificate disponibili:

Evidenziazione di Switch Access della voce di elenco dell'articolo
Figura 6. Evidenziazione di Switch Access della voce di elenco dell'articolo.
Visualizzazione del menu Azioni di Switch Access.
Figura 7. Visualizzazione del menu Azioni di Switch Access.

Albero della semantica

Una composizione descrive l'interfaccia utente della tua app e viene prodotta eseguendo elementi componibili. La composizione è una struttura ad albero costituita dai componibili che descrivono la tua UI.

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

Una tipica gerarchia dell'interfaccia utente e il relativo albero semantico
Figura 8. Una tipica gerarchia della UI e il relativo albero semantico.

Se la tua app è composta da componibili e modificatori della libreria Foundation e Material di Compose, l'albero della semantica viene compilato e generato automaticamente. Tuttavia, quando aggiungi elementi componibili personalizzati di basso livello, devi fornire manualmente la relativa semantica. Potrebbero anche verificarsi situazioni in cui l'albero non rappresenta correttamente o completamente il significato degli elementi sullo schermo. In questo caso, puoi adattare l'albero.

Prendi in considerazione, ad esempio, questo composable di calendario personalizzato:

Un composable di calendario personalizzato con elementi di giorno selezionabili
Figura 9. Un composable di calendario personalizzato con elementi di giorno selezionabili.

In questo esempio, l'intero calendario è implementato come un singolo componibile di basso livello, utilizzando il componibile Layout e disegnando direttamente su Canvas. Se non esegui altre operazioni, i servizi di accessibilità non riceveranno informazioni sufficienti sui contenuti del composable e sulla selezione dell'utente all'interno del calendario. Ad esempio, se un utente fa clic sul giorno contenente 17, il framework di accessibilità riceve solo le informazioni descrittive per l'intero controllo del calendario. In questo caso, il servizio di accessibilità TalkBack annuncerebbe "Calendario" o, solo leggermente meglio, "Calendario di aprile" e l'utente si chiederebbe quale giorno è stato selezionato. Per rendere questo composable più accessibile, dovrai aggiungere manualmente le informazioni semantiche.

Albero unito e diviso

Come accennato in precedenza, ogni componibile nell'albero della UI potrebbe avere zero o più proprietà di semantica impostate. Quando un composable non ha proprietà di semantica impostate, non viene incluso nell'albero della semantica. In questo modo, l'albero della semantica contiene solo i nodi che hanno effettivamente un significato semantico. Tuttavia, a volte, per trasmettere il significato corretto di ciò che viene mostrato sullo schermo, è utile anche unire determinati sottoalberi di nodi e trattarli come un'unica entità. In questo modo, puoi ragionare su un insieme di nodi nel suo complesso, anziché gestire ogni nodo discendente singolarmente. Come regola generale, ogni nodo di questo albero rappresenta un elemento attivabile quando si utilizzano i servizi di accessibilità.

Un esempio di questo tipo di composable è Button. Puoi considerare un pulsante come un 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 della semantica, le proprietà dei discendenti del pulsante vengono unite e il pulsante viene presentato come un singolo nodo foglia nell'albero:

Rappresentazione semantica unita di una singola foglia
Figura 10. Rappresentazione semantica unita di una singola foglia.

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

Questa proprietà è impostata su diversi modificatori e componibili nelle librerie Foundation e Material Compose. Ad esempio, i modificatori clickable e toggleable uniscono automaticamente i relativi discendenti. Inoltre, l'elemento componibile ListItem unirà i suoi discendenti.

Ispeziona l'albero

L'albero semantico è in realtà composto da due alberi diversi. Esiste un albero della semantica unito, che unisce i nodi discendenti quando mergeDescendants è impostato su true. Esiste anche un albero di semantica non unito, che non applica l'unione, ma mantiene intatto ogni nodo. I servizi di accessibilità utilizzano l'albero non unito e applicano i propri algoritmi di unione, tenendo conto della proprietà mergeDescendants. Il framework di test utilizza l'albero unito per impostazione predefinita.

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

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

Layout Inspector consente di visualizzare l'albero della semantica unito e non unito selezionando quello preferito nel filtro di visualizzazione:

Opzioni di visualizzazione di Layout Inspector, che consentono di visualizzare l'albero semantico unito e non unito
Figura 11. Opzioni di visualizzazione di Layout Inspector, che consentono di visualizzare l'albero semantico unito e non unito.

Per ogni nodo della struttura ad albero, Layout Inspector mostra sia la semantica unita sia la semantica impostata su quel nodo nel riquadro delle proprietà:

Proprietà di semantica unite e impostate
Figura 12. Proprietà di semantica unite e impostate.

Per impostazione predefinita, i matcher nel framework di test utilizzano l'albero semantico unito. Ecco perché puoi interagire con un Button abbinando il testo mostrato 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.

Adatta l'albero

Come accennato in precedenza, puoi eseguire l'override o cancellare determinate proprietà di semantica o modificare il comportamento di unione dell'albero. Questo è 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 scoprire di più sui test, consulta la guida ai test.