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.

Considera el tamaño mínimo del objetivo táctil

Todos los elementos de la pantalla en los que se puede hacer clic, que se pueden tocar o con los que se puede interactuar deben ser lo suficientemente grandes para permitir una interacción confiable. Cuando establezcas el tamaño de esos elementos, asegúrate de configurar su tamaño mínimo en 48 dp para seguir los Lineamientos de Accesibilidad de Material Design.

Existen componentes en Material, como Checkbox, RadioButton, Switch, Slider y Surface, que establecen este tamaño mínimo de manera interna, pero solo el usuario puede modificar el componente. Por ejemplo, si Checkbox tiene su parámetro onCheckedChange establecido en un valor no nulo, incluirá un padding para tener un ancho y un alto de 48 dp como mínimo.

@Composable
fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

Cuando el parámetro onCheckedChange se establece en nulo, el padding no está incluido, ya que no se puede interactuar directamente con el componente.

@Composable
fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Cuando implementas controles de selección como Switch, RadioButton o Checkbox, por lo general, quitas el comportamiento de hacer clic en un contenedor superior, configuras la devolución de llamada de clics en el elemento componible a null y agregas un modificador toggleable o selectable al elemento superior que admite composición.

@Composable
fun CheckableRow() {
   MaterialTheme {
       var checked by remember { mutableStateOf(false) }
       Row(
           Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
               .padding(16.dp)
               .fillMaxWidth()
       ) {
           Text("Option", Modifier.weight(1f))
           Checkbox(checked = checked, onCheckedChange = null)
       }
   }
}

Cuando el tamaño de un elemento componible para hacer clic es menor que el tamaño del objetivo táctil mínimo, Compose aumenta el tamaño del objetivo táctil. Para ello, expande el tamaño del objetivo fuera de los límites del elemento componible.

En el siguiente ejemplo, creamos un objeto Box muy pequeño para hacer clic. El área del objetivo táctil se expande automáticamente más allá de los límites de Box, por lo que, cuando se presione junto a Box, se activará el evento de clic.

@Composable
fun DefaultPreview() {
   var clicked by remember { mutableStateOf(false) }
   Box(
       Modifier
           .size(100.dp)
           .background(if (clicked) Color.DarkGray else Color.LightGray)
   ) {
       Box(
           Modifier
               .align(Alignment.Center)
               .clickable { clicked = !clicked }
               .background(Color.Black)
               .size(1.dp)
       )
   }
}

A fin de evitar una posible superposición entre áreas táctiles de diferentes elementos componibles, siempre debes intentar usar un tamaño mínimo lo suficientemente grande para el elemento componible. En nuestro ejemplo, que se muestra a continuación, implicaría el uso del modificador sizeIn para establecer el tamaño mínimo de la casilla interna:

@Composable
fun DefaultPreview() {
   var clicked by remember { mutableStateOf(false) }
   Box(
       Modifier
           .size(100.dp)
           .background(if (clicked) Color.DarkGray else Color.LightGray)
   ) {
       Box(
           Modifier
               .align(Alignment.Center)
               .clickable { clicked = !clicked }
               .background(Color.Black)
               .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
       )
   }
}

Agrega etiquetas de clic

Puedes usar una etiqueta de clic para agregar significado semántico al comportamiento de los clics de un elemento componible. Estas etiquetas de clic describen lo que sucede cuando el usuario interactúa con el elemento componible. Los servicios de accesibilidad usan etiquetas de clic para describirles la app a los usuarios con necesidades específicas.

Para configurar la etiqueta de clics, pasa un parámetro en el modificador clickable:

@Composable
fun ArticleListItem(openArticle: () -> Unit) {
   Row(
       Modifier.clickable(
           // R.string.action_read_article = "read article"
           onClickLabel = stringResource(R.string.action_read_article),
           onClick = openArticle
       )
   ) {
       // ..
   }
}

Como alternativa, si no tienes acceso al modificador en el que se puede hacer clic, puedes configurar la etiqueta de clics en el modificador semantics:

@Composable
fun LowLevelClickLabel(openArticle: () -> Boolean) {
   // R.string.action_read_article = "read article"
   val readArticleLabel = stringResource(R.string.action_read_article)
   Canvas(
       Modifier.semantics {
           onClick(label = readArticleLabel, action = openArticle)
       }
   ) {
       // ..
   }
}

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.

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.

Más información

Para obtener más información sobre la compatibilidad de accesibilidad en tu código de Compose, haz el codelab de accesibilidad en Jetpack Compose.