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; paraImageViewinformativas, texto descriptivo. EnTextViewyButtoncon 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:labelForen elTextViewque etiqueta unEditText; alternativamenteTextInputLayoutconhint, que gestiona la etiqueta correctamente.android:minHeight/minWidthde 48dp para el área táctil, oTouchDelegatepara 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/Afterpara corregir el orden de recorrido cuando la disposición visual y la lógica no coinciden.- Unidades
sppara todo el texto (escalan con el ajuste del sistema); nuncadpni tamaños fijos en texto;android:maxLinescon cuidado ywrap_contenten alturas. android:screenOrientation: no fuercesportraitsalvo 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_SCALEo 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:
contentDescriptionsolo en no textuales informativos (@nullen decorativos),labelFor,accessibilityHeading,accessibilityLiveRegion, 48 dp,sp, sin forzar orientación. - Compose:
semantics { contentDescription, role, stateDescription, heading(), liveRegion, customActions },mergeDescendantspara agrupar,clearAndSetSemantics,traversalIndexpara 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 €)