Accesibilidad en Compose

Las apps escritas en Compose deben admitir soluciones de accesibilidad para usuarios con diferentes necesidades. Los servicios de accesibilidad permiten transformar lo que se muestra en pantalla a un formato más adecuado para un usuario que tiene una necesidad específica. Para brindar compatibilidad con los servicios de accesibilidad, las apps usan API en el framework de Android para exponer información semántica sobre los elementos de su IU. El framework de Android notificará a los servicios de accesibilidad sobre esa información semántica. Cada servicio de accesibilidad puede elegir la mejor forma de describir la app al usuario. Android ofrece varios servicios de accesibilidad, como TalkBack y Accesibilidad con interruptores.

Semántica

Compose usa propiedades de semántica para pasar información a los servicios de accesibilidad. Las propiedades semánticas proporcionan información sobre los elementos de la IU que se muestran al usuario. La mayoría de los elementos integrados componibles como Text y Button completan estas propiedades semánticas con información inferida del elemento componible y sus objetos secundarios. Algunos modificadores, como toggleable y clickable, también establecerán propiedades semánticas. Sin embargo, en algunas ocasiones, el framework necesita más información para comprender cómo describir un elemento de la IU al usuario.

En este documento, se describen varias situaciones en las que necesitas agregar información adicional de manera explícita a un elemento componible para que se pueda describir correctamente al framework de Android. También se explica cómo reemplazar por completo la información semántica para un elemento componible determinado. Se supone que tienes conocimientos básicos sobre la accesibilidad en Android.

Casos de uso comunes

Para ayudar a que los usuarios con necesidades de accesibilidad utilizan tu app correctamente, debes seguir las prácticas recomendadas que se describen en esta página.

Describe elementos visuales

Cuando defines una Image o un Icon componible, no existe una manera automática de que el framework de Android comprenda lo que se muestra. Debes pasar una descripción textual del elemento visual.

Imagina una pantalla en la que el usuario pueda compartir la página actual con amigos. Esta pantalla contiene un ícono para compartir en el que se puede hacer clic:

Una barra de íconos en los que se puede hacer clic, con el ícono para compartir resaltado

Solamente en función del ícono, el framework de Android no puede determinar cómo describirlo a un usuario con discapacidad visual. El framework de Android necesita una descripción textual adicional del elemento.

El parámetro contentDescription se usa para describir un elemento visual. Debes usar una string localizada, ya que esto se comunicará al usuario.

@Composable
fun ShareButton(onClick: () -> Unit) {
  IconButton(onClick = onClick) {
    Icon(
      imageVector = Icons.Filled.Share,
      contentDescription = stringResource(R.string.label_share)
    )
  }
}

Algunos elementos visuales son puramente decorativos, y es posible que no quieras comunicárselos al usuario. Cuando configuras el parámetro contentDescription como null, le indicas al framework de Android que ese elemento no tiene acciones ni estado asociados.

@Composable
fun PostImage(post: Post, modifier: Modifier = Modifier) {
  val image = post.imageThumb ?: imageResource(R.drawable.placeholder_1_1)

  Image(
    bitmap = image,
    // Specify that this image has no semantic meaning
    contentDescription = null,
    modifier = modifier
      .size(40.dp, 40.dp)
      .clip(MaterialTheme.shapes.small)
  )
}

Depende de ti decidir si un elemento visual específico necesita una contentDescription. Pregúntate si el elemento transmite información que el usuario necesitará para realizar su tarea. De lo contrario, es mejor no incluir una descripción.

Establece descripciones personalizadas de contenido

Puede que existan situaciones en las que quieras establecer una descripción de contenido de manera explícita. En esas situaciones, puedes usar el modificador semantics para asignar de forma directa una descripción de contenido a un elemento componible. Por ejemplo, posiblemente quieras dibujar un ícono personalizado, lo que significa que no puedes usar de manera directa el elemento componible Icon.

@Composable
fun CustomShareIcon(modifier: Modifier = Modifier) {
  val cd = stringResource(R.string.custom_share_icon_content_description)
  Canvas(modifier.semantics { contentDescription = cd }) {
    /* Draw custom icon here */
  }
}

Combina elementos

Los servicios de accesibilidad como TalkBack y Accesibilidad con interruptores permiten que los usuarios muevan el enfoque entre los elementos de la pantalla. Es importante que los elementos estén enfocados en el nivel de detalle correcto. Si todos los elementos componibles de bajo nivel están centrados de forma independiente, el usuario tendrá que interactuar mucho para moverse por toda la pantalla. Si los elementos se combinan de manera demasiado agresiva, es posible que los usuarios no comprendan qué elementos pertenecen a ellos.

Cuando aplicas un modificador clickable en un elemento componible, Compose fusiona automáticamente todos los elementos que contiene. Esto también se aplica a ListItem; los elementos dentro de un elemento de lista se fusionarán, y los servicios de accesibilidad los verán como un elemento.

Se puede formar un grupo lógico con un conjunto de elementos componibles, pero ese grupo no admitirá clics ni será parte de un elemento de lista. Querrás que los servicios de accesibilidad los vean como un solo elemento. Por ejemplo, imagina un elemento componible que muestra el avatar de un usuario, su nombre y algún dato adicional:

Grupo de elementos de la IU que incluye el nombre de un usuario. Se selecciona el nombre.

Puedes indicarle a Compose que combine esos elementos mediante el parámetro mergeDescendants en el modificador semantics. De esta manera, los servicios de accesibilidad seleccionarán solo el elemento combinado, y se combinarán todas las propiedades semánticas de los elementos subordinados.

@Composable
private fun PostMetadata(metadata: Metadata) {
  // Merge elements below for accessibility purposes
  Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
    Image(
      imageVector = Icons.Filled.AccountCircle,
      contentDescription = null // decorative
    )
    Column {
      Text(metadata.author.name)
      Text("${metadata.date} • ${metadata.readTimeMinutes} min read")
    }
  }
}

Los servicios de accesibilidad ahora se enfocarán en todo el contenedor de una vez, lo que combinará su contenido:

Grupo de elementos de la IU que incluye el nombre de un usuario. Se seleccionan todos los elementos.

Agrega acciones personalizadas

Observa el siguiente elemento de la lista:

Un elemento de lista común que contiene un título, un autor y un ícono de favorito.

Cuando usas un lector de pantalla como TalkBack para escuchar lo que se muestra, primero se selecciona todo el elemento y, luego, el ícono de favorito.

El elemento de lista, con todos los elementos seleccionados juntos.

El elemento de lista, que tiene solo el ícono de favorito seleccionado

En una lista larga, esto puede ser muy repetitivo. Un mejor enfoque sería definir una acción personalizada que le permita a un usuario agregar el elemento a favoritos. Recuerda que también deberás quitar explícitamente el comportamiento del ícono de favoritos para asegurarte de que no lo seleccione el servicio de accesibilidad. Para ello, usa el modificador clearAndSetSemantics:

@Composable
fun PostCardSimple(
  /* ... */
  isFavorite: Boolean,
  onToggleFavorite: () -> Boolean
) {
  val actionLabel = stringResource(
    if (isFavorite) R.string.unfavorite else R.string.favorite
  )
  Row(modifier = Modifier
    .clickable(onClick = { /* ... */ })
    .semantics {
      // Set any explicit semantic properties
      customActions = listOf(
        CustomAccessibilityAction(actionLabel, onToggleFavorite)
      )
    }
  ) {
    /* ... */
    BookmarkButton(
      isBookmarked = isFavorite,
      onClick = onToggleFavorite,
      // Clear any semantics properties set on this node
      modifier = Modifier.clearAndSetSemantics { }
    )
  }
}

Describe el estado de un elemento

Un elemento componible puede definir una stateDescription para la semántica que usa el framework de Android a fin de leer el estado en el que está el elemento. Por ejemplo, un elemento componible que se puede activar o desactivar puede aparecer como "Checked" ("Verificado") o "Unchecked" ("No verificado"). En algunos casos, puede que quieras anular las etiquetas de descripción de estado predeterminadas que usa Compose. Puedes hacerlo especificando explícitamente las etiquetas de descripción de estado antes de definir un elemento componible como un elemento que se puede activar o desactivar:

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

Define encabezados

En algunas ocasiones, las apps muestran mucho contenido en una pantalla, en un contenedor desplazable. Por ejemplo, una pantalla puede mostrar el contenido completo de un artículo que el usuario está leyendo:

Captura de pantalla de una entrada de blog, con el texto del artículo en un contenedor desplazable.

Los usuarios con necesidades de accesibilidad tendrán dificultades para navegar por esta pantalla. Para facilitar la navegación, puedes indicar qué elementos son encabezados. En el ejemplo anterior, cada título de subsección se puede definir como un encabezado para los fines de accesibilidad. Algunos servicios de accesibilidad, como TalkBack, permiten que los usuarios naveguen directamente de un encabezado a otro.

En Compose, para indicar que un elemento componible es un encabezado, define su propiedad semántica:

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

Cómo crear elementos componibles personalizados de bajo nivel

Un caso de uso más avanzado implica reemplazar ciertos componentes de Material de tu app por versiones personalizadas. En estos casos, es fundamental que tengas en cuenta las consideraciones de accesibilidad. Supongamos que reemplazas el objeto Checkbox de Material con tu propia implementación. Sería muy fácil olvidar agregar el modificador triStateToggleable, que administra las propiedades de accesibilidad de este componente.

Como regla general, debes tener en cuenta la implementación del componente en la biblioteca de Material e imitar cualquier comportamiento de accesibilidad que encuentres. Además, usa activamente los modificadores de Foundation, en lugar de los modificadores del nivel de IU, ya que incluyen consideraciones de accesibilidad de forma inmediata. Asegúrate de probar la implementación de tus componentes personalizados con varios servicios de accesibilidad para verificar su comportamiento.