Addaw, anar a la pàgina d'inici Donar

Lliçó 4 de 10 · 12 min de lectura · Revisada el 26/10/2026

Android: TalkBack, contentDescription, semàntica de Compose i Switch Access

Estàs llegint una lliçó oberta. Matricula't per fer els tests i els exercicis, desar el teu progrés i aconseguir el certificat.

Matricula-m'hi · nivell 6 (119 €)

A Android, l'accessibilitat passa pel servei d'accessibilitat: TalkBack, Switch Access, Accés per veu i altres consulten l'arbre de nodes AccessibilityNodeInfo que el framework construeix a partir de les vistes (sistema View) o de la semàntica (Jetpack Compose). Aquesta lliçó explica què ha d'exposar el desenvolupador en cada sistema, amb codi Kotlin, i com provar-ho amb TalkBack i Switch Access.

TalkBack: l'imprescindible

Activa TalkBack amb els dos botons de volum (drecera d'accessibilitat) o a Configuració > Accessibilitat. Gestos: lliscar a la dreta/esquerra per a l'element següent/anterior, doble toc per activar, lliscar amunt/avall per canviar la granularitat o el mode de lectura, el menú de TalkBack (toc amb tres dits o lliscar cap avall i després a la dreta) per a les accions. TalkBack anuncia per a cada node: descripció o text, estat (marcat, seleccionat, desplegat), rol («botó», «casella»), accions («toca dues vegades per activar») i, si n'hi ha, accions personalitzades al menú. Activa a TalkBack > Configuració per a desenvolupadors l'opció «Mostra la sortida de veu» per veure a la pantalla el que s'anuncia: imprescindible per auditar i per enregistrar evidències.

Sistema View (XML i Kotlin)

  • android:contentDescription: el nom accessible de les imatges i dels botons d'icona. Per a les imatges decoratives, @null; per a les ImageView informatives, un text descriptiu. A TextView i Button amb text no cal: el text és el nom.
  • android:importantForAccessibility="no" per amagar un element al servei; "noHideDescendants" per amagar també els seus fills (per exemple, el contingut darrere d'un diàleg).
  • android:labelFor al TextView que etiqueta un EditText; alternativament TextInputLayout amb hint, que gestiona l'etiqueta correctament.
  • android:minHeight/minWidth de 48dp per a l'àrea tàctil, o TouchDelegate per ampliar l'àrea sense canviar la mida visual.
  • android:accessibilityHeading="true" (API 28) per als encapçalaments; TalkBack permet navegar-hi.
  • android:accessibilityLiveRegion="polite" per a les zones el text de les quals canvia i s'ha d'anunciar (missatges d'estat, 4.1.3). announceForAccessibility() està desaconsellat i figura com a obsolet des de l'API 36 (Android 16): fes servir live regions o, per als canvis de finestra, setAccessibilityPaneTitle.
  • android:accessibilityTraversalBefore/After per corregir l'ordre de recorregut quan la disposició visual i la lògica no coincideixen.
  • Unitats sp per a tot el text (s'escalen amb l'ajust del sistema); mai dp ni mides fixes al text; android:maxLines amb compte i wrap_content a les alçades.
  • android:screenOrientation: no forcis portrait llevat que sigui essencial (1.3.4).

Per a una vista personalitzada (un gràfic, un control de puntuació), implementa un AccessibilityDelegate que empleni el node amb el rol, l'estat i les accions:

class PuntuacionView(context: Context) : View(context) {
    var valor = 3
    init {
        isFocusable = true
        ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
            override fun onInitializeAccessibilityNodeInfo(
                host: View, info: AccessibilityNodeInfoCompat
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info.className = SeekBar::class.java.name     // rol: control ajustable
                info.contentDescription = "Valoració"
                info.stateDescription = "$valor de 5 estrelles"
                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)
            }
            override fun performAccessibilityAction(
                host: View, action: Int, args: Bundle?
            ): Boolean = when (action) {
                AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> { cambiar(+1); true }
                AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> { cambiar(-1); true }
                else -> super.performAccessibilityAction(host, action, args)
            }
        })
    }
    private fun cambiar(d: Int) {
        valor = (valor + d).coerceIn(1, 5); invalidate()
        ViewCompat.setStateDescription(this, "$valor de 5 estrelles")
    }
}

Punt clau

Abans d'escriure un delegat d'accessibilitat, pregunta't si pots fer servir un component estàndard (MaterialButton, Slider, CheckBox, Chip). Els components de Material ja exposen el rol, l'estat i les accions, i es mantenen al dia amb cada versió d'Android. La vista personalitzada és l'últim recurs.

Jetpack Compose: semàntica

A Compose no hi ha vistes: hi ha un arbre de semàntica construït pels composables. Els components de Material 3 (Button, Checkbox, Switch, TextField) aporten el rol i l'estat. El desenvolupador completa el que falta amb el modificador semantics:

@Composable
fun FilaMovimiento(m: Movimiento, onAbrir: () -> Unit, onCategorizar: () -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .heightIn(min = 48.dp)
            .clickable(role = Role.Button, onClick = onAbrir) // rol i acció
            .semantics(mergeDescendants = true) {  // un sol node
                contentDescription = "${m.concepto}, ${m.importeAccesible}, ${m.fechaLarga}"
                customActions = listOf(
                    CustomAccessibilityAction("Categoritza") { onCategorizar(); true }
                )
            }
    ) {
        Icon(painterResource(m.icono), contentDescription = null) // decoratiu
        Column(Modifier.weight(1f)) {
            Text(m.concepto, style = MaterialTheme.typography.bodyLarge)
            Text(m.fechaFormateada, style = MaterialTheme.typography.bodySmall)
        }
        Text(m.importeFormateado)
    }
}

// Encapçalament de secció
Text(
    "Moviments de març",
    style = MaterialTheme.typography.titleMedium,
    modifier = Modifier.semantics { heading() }
)

// Botó d'icona: sempre amb descripció
IconButton(onClick = onBuscar) {
    Icon(Icons.Default.Search, contentDescription = "Cerca")
}

// Missatge d'estat que s'ha d'anunciar en canviar
Text(
    estado,
    modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }
)

Altres elements de l'API de semàntica: role = Role.Button (o Checkbox, Switch, Tab, Image) quan construeixes un control a mà; stateDescription per als estats («Desplegat»); clearAndSetSemantics { } per substituir completament la semàntica dels fills; isTraversalGroup = true i traversalIndex dins de semantics per controlar l'ordre de recorregut (Compose 1.5+); Modifier.minimumInteractiveComponentSize() (l'apliquen per defecte els components de Material) per garantir 48 dp; Modifier.focusRequester per moure el focus al primer error; Modifier.toggleable/selectable en lloc de clickable per als elements amb estat, perquè hi afegeixen el rol i l'estat automàticament.

Per a l'escalat del text, fes servir sp (els estils de tipografia de Material ja ho fan) i comprova-ho amb «Mida de la lletra» al màxim i «Mida de visualització» gran. Les alçades fixes i maxLines = 1 amb overflow = Ellipsis en textos essencials són l'error més comú.

Error freqüent

Posar contentDescription a tot, inclosos els textos i els contenidors, «perquè ho llegeixi TalkBack». El resultat és soroll: el text s'anuncia dues vegades, els contenidors es converteixen en elements enfocables buits i les imatges decoratives interrompen la lectura. La regla: descripció només als elements no textuals informatius; null als decoratius; agrupació amb mergeDescendants quan diversos textos formen una unitat.

Switch Access, Accés per veu i teclat

Switch Access permet fer servir Android amb un o dos commutadors: recorre els elements enfocables i accionables (o escaneja per files/punts) i activa el seleccionat. Requereix que tots els controls siguin focusable i clickable a nivell d'accessibilitat, que l'ordre sigui lògic i que no hi hagi accions només per gest. Accés per veu («Toca Cerca», «Mostra els números») fa servir el text visible o el contentDescription: els botons d'icona sense descripció són inabastables. Els teclats físics (molt utilitzats per persones amb discapacitat motriu) exigeixen android:focusable i nextFocusDown/Up coherents; a Compose, Modifier.focusable() i focusProperties. L'indicador de focus (2.4.7) ha de ser visible: els components de Material el pinten; als controls propis cal dibuixar-lo amb Modifier.onFocusChanged o un state_focused al drawable.

Ajustos del sistema que l'aplicació ha de respectar

  • Suprimeix les animacions (Configuració > Accessibilitat): consulta Settings.Global.ANIMATOR_DURATION_SCALE o deixa que les animacions del framework ho respectin.
  • Text d'alt contrast, Inversió de colors i Correcció de color: fes servir els colors del tema i comprova-ho en mode fosc.
  • Temps per fer accions (AccessibilityManager.getRecommendedTimeoutMillis): s'aplica als toasts, als snackbars i als missatges temporals (2.2.1).
  • Àudio mono i subtítols: per a les aplicacions amb vídeo, respecta les preferències de CaptioningManager.

Exemple

Aplicació de cita prèvia d'un centre de salut: la llista de forats fa servir Chip seleccionables (rol i estat automàtics), el calendari personalitzat exposa cada dia amb role = Role.Button, un contentDescription amb la data completa i un stateDescription «Sense forats» o «3 forats»; en confirmar, un Text amb liveRegion anuncia «Cita confirmada per al 12 de maig a les 9:30», i el focus passa al botó «Afegeix al calendari». Tot funciona amb TalkBack, Switch Access i teclat.

Resum

  • TalkBack anuncia la descripció, l'estat, el rol i les accions del node d'accessibilitat; «Mostra la sortida de veu» permet veure què s'anuncia.
  • Views: contentDescription només als no textuals informatius (@null als decoratius), labelFor, accessibilityHeading, accessibilityLiveRegion, 48 dp, sp, sense forçar l'orientació.
  • Compose: semantics { contentDescription, role, stateDescription, heading(), liveRegion, customActions }, mergeDescendants per agrupar, clearAndSetSemantics, traversalIndex per a l'ordre.
  • Prefereix els components de Material; els delegats i les semàntiques manuals són l'últim recurs.
  • Switch Access, Accés per veu i el teclat exigeixen controls enfocables, amb nom i amb focus visible, i res d'accions només per gest.

T'ha servit aquesta lliçó?

Crea el teu compte i segueix el curs amb tot:

  • El test d'aquesta lliçó (3 preguntes) per comprovar el que has après
  • Exercicis pràctics amb pàgines reals
  • El teu progrés desat: continues on ho vas deixar, des de qualsevol dispositiu
  • Constància del curs amb les hores i certificat del nivell 6 quan aprovis l'examen

Entres amb el teu compte de Google o només amb el teu correu electrònic. No cal contrasenya.

Matricula-m'hi · nivell 6 (119 €)