Pasos clave para mejorar la accesibilidad de Compose

Para ayudar a las personas con necesidades de accesibilidad a usar tu app correctamente, diseña la app de manera que sea compatible con los requisitos clave de accesibilidad.

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 estos elementos, asegúrate de establecer el tamaño mínimo en 48 dp para seguir correctamente los lineamientos de accesibilidad de Material Design.

Los componentes de Material, como Checkbox, RadioButton, Switch, Slider y Surface, establecen este tamaño mínimo de forma interna, pero solo cuando el componente puede recibir acciones del usuario. Por ejemplo, cuando un Checkbox tiene su parámetro onCheckedChange establecido en un valor no nulo, la casilla de verificación incluye un padding para tener un ancho y una altura de al menos 48 dp.

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

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

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

Figura 1: Una casilla de verificación sin padding

Cuando implementas controles de selección como Switch, RadioButton o Checkbox, por lo general, debes subir el comportamiento en el que se puede hacer clic a un contenedor superior, establecer la devolución de llamada de clics en el elemento componible en null y agregar un modificador toggleable o selectable al elemento superior componible.

@Composable
private 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 táctil fuera de los límites del elemento componible.

El siguiente ejemplo contiene una Box muy pequeña en la que se puede 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 presiona junto a Box, se activa el evento de clic.

@Composable
private fun SmallBox() {
    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)
        )
    }
}

Para evitar una posible superposición entre áreas táctiles de diferentes elementos componibles, siempre usa un tamaño mínimo lo suficientemente grande para el elemento. En el ejemplo, eso implicaría usar el modificador sizeIn para establecer el tamaño mínimo del cuadro interno:

@Composable
private fun LargeBox() {
    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. Las etiquetas de clic describen lo que sucede cuando el usuario interactúa con el elemento componible. Los servicios de accesibilidad usan etiquetas de clics para ayudar a describir la app a los usuarios con necesidades específicas.

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

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

De manera alternativa, si no tienes acceso al modificador en el que se puede hacer clic, configura la etiqueta de clics en el modificador semántica:

@Composable
private 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 un elemento componible Image o Icon, no hay una forma automática para que el framework de Android comprenda lo que muestra la app. 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

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

El parámetro contentDescription describe un elemento visual. Usa una cadena localizada, ya que es visible para el usuario.

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

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

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

    Image(
        painter = 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 la 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. Cuando cada elemento componible de bajo nivel de la pantalla se enfoca de forma independiente, los usuarios tienen que interactuar mucho para moverse por la pantalla. Si los elementos se fusionan de manera demasiado agresiva, es posible que los usuarios no comprendan qué elementos deben estar juntos.

Cuando aplicas un modificador clickable a un elemento componible, Compose combina automáticamente todos los elementos que este contiene. Esto también se aplica a ListItem. Los elementos dentro de un elemento de lista se combinan y los servicios de accesibilidad los ven como uno solo.

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 cierta información adicional:

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

Puedes habilitar Compose para combinar estos elementos con el parámetro mergeDescendants en el modificador semantics. De esta manera, los servicios de accesibilidad seleccionan solo el elemento combinado y se combinan 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 enfocan en el contenedor completo a la vez y combinan 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 es definir una acción personalizada que le permita al usuario agregar el elemento a favoritos. Ten en cuenta que también debes quitar de forma explícita el comportamiento del ícono de favorito para asegurarte de que no lo seleccione el servicio de accesibilidad. Para ello, usa el modificador clearAndSetSemantics:

@Composable
private 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 { }
        )
    }
}

Describir el estado de un elemento

Un elemento componible puede definir un stateDescription para la semántica que usa el framework de Android a fin de leer el estado en el que se encuentra el elemento. Por ejemplo, un elemento componible que se puede activar o desactivar puede estar en estado "marcado" o "no marcado". En algunos casos, es posible que quieras anular las etiquetas de descripción de estado predeterminadas que usa Compose. Para ello, especifica de forma explícita las etiquetas de descripción del estado antes de definir un elemento componible como elemento que se pueda 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

A veces, las apps muestran mucho contenido en una pantalla dentro de 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 tienen dificultades para navegar por esa pantalla. Para facilitar la navegación, indica qué elementos son encabezados. En el ejemplo anterior, cada título de la subsección se puede definir como un encabezado por cuestiones 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, debes definir su propiedad semantics:

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

Cómo controlar elementos componibles personalizados

Cada vez que reemplaces ciertos componentes de Material en tu app con versiones personalizadas, debes tener en cuenta las consideraciones de accesibilidad.

Supongamos que reemplazas el elemento Checkbox de Material con tu propia implementación. Podrías olvidarte de agregar el modificador triStateToggleable, que controla las propiedades de accesibilidad de este componente.

Como regla general, observa la implementación del componente en la biblioteca de Material e imita 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.

Prueba la implementación de tus componentes personalizados con varios servicios de accesibilidad para verificar su comportamiento.

Recursos adicionales