Addaw, ir a inicio Donar

Lección 4 de 10 · 12 min de lectura · Revisado el 26/10/2026

Android: TalkBack, contentDescription, semántica de Compose y Switch Access

En Android, la accesibilidad pasa por el servicio de accesibilidad: TalkBack, Switch Access, Acceso por voz y otros consultan el árbol de nodos AccessibilityNodeInfo que el framework construye a partir de las vistas (sistema View) o de la semántica (Jetpack Compose). Esta lección explica qué debe exponer el desarrollador en cada sistema, con código Kotlin, y cómo probarlo con TalkBack y Switch Access.

TalkBack: lo imprescindible

Activa TalkBack con los dos botones de volumen (atajo de accesibilidad) o en Ajustes > Accesibilidad. Gestos: deslizar derecha/izquierda para siguiente/anterior, doble toque para activar, deslizar arriba/abajo para cambiar la granularidad o el modo de lectura, el menú de TalkBack (toque con tres dedos o deslizar hacia abajo y luego a la derecha) para las acciones. TalkBack anuncia para cada nodo: descripción o texto, estado (marcado, seleccionado, expandido), rol («botón», «casilla»), acciones («pulsa dos veces para activar») y, si hay, acciones personalizadas en el menú. Activa en TalkBack > Ajustes de desarrollador la opción «Mostrar salida de voz» para ver en pantalla lo que se anuncia: imprescindible para auditar y para grabar evidencias.

Sistema View (XML y Kotlin)

  • android:contentDescription: el nombre accesible de imágenes y botones de icono. Para imágenes decorativas, @null; para ImageView informativas, texto descriptivo. En TextView y Button con texto no hace falta: el texto es el nombre.
  • android:importantForAccessibility="no" para ocultar un elemento del servicio; "noHideDescendants" para ocultar también sus hijos (por ejemplo, el contenido tras un diálogo).
  • android:labelFor en el TextView que etiqueta un EditText; alternativamente TextInputLayout con hint, que gestiona la etiqueta correctamente.
  • android:minHeight/minWidth de 48dp para el área táctil, o TouchDelegate para ampliar el área sin cambiar el tamaño visual.
  • android:accessibilityHeading="true" (API 28) para encabezados; TalkBack permite navegar por ellos.
  • android:accessibilityLiveRegion="polite" para zonas cuyo texto cambia y debe anunciarse (mensajes de estado, 4.1.3). announceForAccessibility() está desaconsejado y figura como obsoleto desde API 36 (Android 16): usa live regions o, para cambios de ventana, setAccessibilityPaneTitle.
  • android:accessibilityTraversalBefore/After para corregir el orden de recorrido cuando la disposición visual y la lógica no coinciden.
  • Unidades sp para todo el texto (escalan con el ajuste del sistema); nunca dp ni tamaños fijos en texto; android:maxLines con cuidado y wrap_content en alturas.
  • android:screenOrientation: no fuerces portrait salvo que sea esencial (1.3.4).

Para una vista personalizada (un gráfico, un control de puntuación), implementa un AccessibilityDelegate que rellene el nodo con rol, estado y acciones:

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ón"
                info.stateDescription = "$valor de 5 estrellas"
                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 estrellas")
    }
}

Punto clave

Antes de escribir un delegado de accesibilidad, pregúntate si puedes usar un componente estándar (MaterialButton, Slider, CheckBox, Chip). Los componentes de Material ya exponen rol, estado y acciones, y se mantienen al día con cada versión de Android. La vista personalizada es el último recurso.

Jetpack Compose: semántica

En Compose no hay vistas: hay un árbol de semántica construido por los composables. Los componentes de Material 3 (Button, Checkbox, Switch, TextField) aportan rol y estado. El desarrollador completa lo que falta con 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 y acción
            .semantics(mergeDescendants = true) {  // un solo nodo
                contentDescription = "${m.concepto}, ${m.importeAccesible}, ${m.fechaLarga}"
                customActions = listOf(
                    CustomAccessibilityAction("Categorizar") { onCategorizar(); true }
                )
            }
    ) {
        Icon(painterResource(m.icono), contentDescription = null) // decorativo
        Column(Modifier.weight(1f)) {
            Text(m.concepto, style = MaterialTheme.typography.bodyLarge)
            Text(m.fechaFormateada, style = MaterialTheme.typography.bodySmall)
        }
        Text(m.importeFormateado)
    }
}

// Encabezado de sección
Text(
    "Movimientos de marzo",
    style = MaterialTheme.typography.titleMedium,
    modifier = Modifier.semantics { heading() }
)

// Botón de icono: siempre con descripción
IconButton(onClick = onBuscar) {
    Icon(Icons.Default.Search, contentDescription = "Buscar")
}

// Mensaje de estado que debe anunciarse al cambiar
Text(
    estado,
    modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }
)

Otros elementos de la API de semántica: role = Role.Button (o Checkbox, Switch, Tab, Image) cuando construyes un control a mano; stateDescription para estados («Expandido»); clearAndSetSemantics { } para sustituir por completo la semántica de los hijos; isTraversalGroup = true y traversalIndex dentro de semantics para controlar el orden de recorrido (Compose 1.5+); Modifier.minimumInteractiveComponentSize() (lo aplican por defecto los componentes de Material) para garantizar 48 dp; Modifier.focusRequester para mover el foco al primer error; Modifier.toggleable/selectable en lugar de clickable para elementos con estado, porque añaden rol y estado automáticamente.

Para el escalado de texto, usa sp (los estilos de tipografía de Material ya lo hacen) y comprueba con «Tamaño de fuente» al máximo y «Tamaño de visualización» grande. Las alturas fijas y maxLines = 1 con overflow = Ellipsis en textos esenciales son el fallo más común.

Error frecuente

Poner contentDescription en todo, incluidos textos y contenedores, «para que lo lea TalkBack». El resultado es ruido: el texto se anuncia dos veces, los contenedores se convierten en elementos enfocables vacíos y las imágenes decorativas interrumpen la lectura. La regla: descripción solo en elementos no textuales informativos; null en decorativos; agrupación con mergeDescendants cuando varios textos forman una unidad.

Switch Access, Acceso por voz y teclado

Switch Access permite usar Android con uno o dos pulsadores: recorre los elementos enfocables y accionables (o escanea por filas/puntos) y activa el seleccionado. Requiere que todos los controles sean focusable y clickable a nivel de accesibilidad, que el orden sea lógico y que no haya acciones solo por gesto. Acceso por voz («Pulsa Buscar», «Mostrar números») usa el texto visible o el contentDescription: botones de icono sin descripción son inalcanzables. Los teclados físicos (muy usados por personas con discapacidad motriz) exigen android:focusable y nextFocusDown/Up coherentes; en Compose, Modifier.focusable() y focusProperties. El indicador de foco (2.4.7) debe ser visible: los componentes de Material lo pintan; en controles propios hay que dibujarlo con Modifier.onFocusChanged o un state_focused en el drawable.

Ajustes del sistema que la app debe respetar

  • Eliminar animaciones (Ajustes > Accesibilidad): consulta Settings.Global.ANIMATOR_DURATION_SCALE o deja que las animaciones del framework lo respeten.
  • Texto de alto contraste, Inversión de colores y Corrección de color: usa colores de tema y comprueba en modo oscuro.
  • Tiempo para realizar acciones (AccessibilityManager.getRecommendedTimeoutMillis): aplica a toasts, snackbars y mensajes temporales (2.2.1).
  • Audio mono y subtítulos: para apps con vídeo, respeta las preferencias de CaptioningManager.

Ejemplo

App de cita previa de un centro de salud: la lista de huecos usa Chip seleccionables (rol y estado automáticos), el calendario personalizado expone cada día con role = Role.Button, contentDescription con la fecha completa y stateDescription «Sin huecos» o «3 huecos»; al confirmar, una Text con liveRegion anuncia «Cita confirmada para el 12 de mayo a las 9:30», y el foco pasa al botón «Añadir al calendario». Todo funciona con TalkBack, Switch Access y teclado.

Resumen

  • TalkBack anuncia descripción, estado, rol y acciones del nodo de accesibilidad; «Mostrar salida de voz» permite ver qué se anuncia.
  • Views: contentDescription solo en no textuales informativos (@null en decorativos), labelFor, accessibilityHeading, accessibilityLiveRegion, 48 dp, sp, sin forzar orientación.
  • Compose: semantics { contentDescription, role, stateDescription, heading(), liveRegion, customActions }, mergeDescendants para agrupar, clearAndSetSemantics, traversalIndex para el orden.
  • Prefiere componentes de Material; los delegados y semánticas manuales son el último recurso.
  • Switch Access, Acceso por voz y teclado exigen controles enfocables, con nombre y con foco visible, y nada de acciones solo por gesto.

Comprueba lo que has aprendido

Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.

Matricularme · nivel 6 (119 €)