contentDescription en XML
<!-- ImageView informativa — siempre con descripción -->
<ImageView
android:contentDescription="@string/foto_perfil_desc"
android:src="@drawable/foto_perfil" />
<!-- ImageView decorativa — vaciar la descripción -->
<ImageView
android:contentDescription="@null"
android:src="@drawable/fondo_decorativo"
android:importantForAccessibility="no" />
<!-- ImageButton siempre necesita descripción -->
<ImageButton
android:contentDescription="@string/btn_buscar"
android:src="@drawable/ic_search" />
<!-- Desde código — cuando la descripción es dinámica -->
// En el Fragment/Activity:
binding.imagenProducto.contentDescription =
"Foto del producto ${producto.nombre}"
// Para actualizar cuando cambia el estado:
binding.btnFavorito.contentDescription = if (esFavorito)
getString(R.string.quitar_favorito, producto.nombre)
else
getString(R.string.agregar_favorito, producto.nombre)
importantForAccessibility
<!-- Valores posibles: -->
<!-- auto (default): Android decide según el tipo de View -->
<!-- yes: siempre visible para accesibilidad -->
<!-- no: invisible para accesibilidad -->
<!-- noHideDescendants: ni la View ni sus hijos son visibles -->
<!-- Ocultar elemento decorativo completamente -->
<View
android:importantForAccessibility="no"
android:background="@drawable/separador_decorativo" />
<!-- Ocultar un contenedor y todo su contenido -->
<LinearLayout
android:importantForAccessibility="noHideDescendants">
<!-- Todo lo de aquí adentro es invisible para TalkBack -->
</LinearLayout>
// Desde código — útil cuando el estado cambia:
binding.panelCarga.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
// Cuando el contenido está listo:
binding.panelCarga.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
labelFor — asociar etiquetas con campos
<!-- labelFor asocia un TextView como etiqueta de un campo de entrada -->
<!-- TalkBack anuncia la etiqueta cuando el usuario enfoca el campo -->
<TextView
android:id="@+id/lbl_email"
android:text="Correo electrónico"
android:labelFor="@id/et_email" />
<EditText
android:id="@+id/et_email"
android:inputType="textEmailAddress"
android:hint="[email protected]" />
<!-- TalkBack anuncia: "Correo electrónico. [email protected]. Campo de texto. -->
<!-- Para TextInputLayout (Material) — la etiqueta ya está integrada -->
<com.google.android.material.textfield.TextInputLayout
android:hint="Correo electrónico">
<com.google.android.material.textfield.TextInputEditText
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<!-- TextInputLayout maneja la accesibilidad automáticamente -->
ViewGroups — agrupar para TalkBack
<!-- focusable en el contenedor agrupa todos los elementos para TalkBack -->
<LinearLayout
android:focusable="true"
android:contentDescription="Carlos Pensa, Desarrollador Android">
<ImageView
android:contentDescription="@null"
android:importantForAccessibility="no"
android:src="@drawable/avatar" />
<LinearLayout android:importantForAccessibility="noHideDescendants">
<TextView android:text="Carlos Pensa" />
<TextView android:text="Desarrollador Android" />
</LinearLayout>
</LinearLayout>
<!-- TalkBack enfoca el LinearLayout externo y lee la descripción del grupo -->
ViewCompat.setAccessibilityDelegate — comportamiento custom
// accessibilityDelegate permite definir comportamiento complejo de accesibilidad
// sin subclasear la View
ViewCompat.setAccessibilityDelegate(binding.cardProducto,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
// Agregar acciones custom
info.addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK,
"Ver detalles del producto"
)
)
// Establecer el role
info.roleDescription = "Card de producto"
// Estado
info.stateDescription = if (producto.disponible)
"Disponible" else "Sin stock"
}
override fun performAccessibilityAction(
host: View,
action: Int,
args: Bundle?
): Boolean {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
navegarADetalle(producto)
return true
}
return super.performAccessibilityAction(host, action, args)
}
}
)
RecyclerView accesible
// El ViewHolder debe tener semántica completa
class ProductoViewHolder(binding: ItemProductoBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(producto: Producto) {
binding.apply {
tvNombre.text = producto.nombre
tvPrecio.text = "$${producto.precio}"
ivFoto.contentDescription = "Foto de ${producto.nombre}"
// La card completa debería ser navegable como una unidad
root.contentDescription =
"${producto.nombre}, $${producto.precio}" +
if (!producto.disponible) ", Sin stock" else ""
// Marcar que la card es clickable (TalkBack lo anunciará)
root.isClickable = true
root.isFocusable = true
btnAgregarCarrito.contentDescription =
"Agregar ${producto.nombre} al carrito"
}
}
}
// Para listas muy largas — anunciar la posición:
// "Elemento 1 de 50"
override fun onBindViewHolder(holder: ProductoViewHolder, position: Int) {
val producto = productos[position]
holder.bind(producto)
ViewCompat.setAccessibilityDelegate(holder.itemView,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View, info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.collectionItemInfo =
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
position, 1, 0, 1, false
)
}
}
)
}