Pasos clave para mejorar la accesibilidad de Compose

Para ayudar a las personas con necesidades de accesibilidad a usar tu app de forma exitosa, diseña tu app para cumplir con 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 dimensiones estos elementos, asegúrate de establece el tamaño mínimo en 48 dp para cumplir con la norma de Material Design lineamientos de accesibilidad.

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

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

Cuando el parámetro onCheckedChange se establece como nulo, el padding no se porque no se puede interactuar directamente con él.

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

Figura 1: Una casilla de verificación sin padding

Cuando implementes controles de selección, como Switch, RadioButton o Checkbox, por lo general, elevas el comportamiento de hacer clic a un contenedor superior, establecido la devolución de llamada de clic en el elemento componible para null y agrega toggleable o selectable al elemento componible superior.

@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, se expande el tamaño del objetivo táctil fuera de los límites del elemento componible.

El siguiente ejemplo contiene un elemento Box muy pequeño en el que se puede hacer clic. El objetivo táctil se expande automáticamente más allá de los límites de Box, por lo que si presionas junto a Box seguirá activando 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 componible. En el ejemplo, eso sería implica usar el modificador sizeIn para establecer el tamaño mínimo de la caja interna:

@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 componible. Los servicios de accesibilidad usan etiquetas de clic para ayudar a describir la app a usuarios con necesidades específicas.

Para configurar la etiqueta de clic, 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
        )
    ) {
        // ..
    }
}

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

@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 Image o Icon componible, no hay forma automática para que el framework de Android comprenda qué es la app mostrando. 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 la

Basándose solamente en el ícono, el framework de Android no puede describirlo a un usuario con discapacidad. El framework de Android necesita una descripción textual adicional de el ícono.

El parámetro contentDescription describe un elemento visual. Usa una dirección de correo electrónico cadena, 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 puramente decorativos, y es posible que no quieras comunicar con el usuario. Cuando configuras el parámetro contentDescription en null, le indican al framework de Android que este elemento no está asociado acciones o estados.

@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 deberá realizar la tarea. Si no es así, es mejor dejar describe.

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 todos los elementos componibles de bajo nivel de la pantalla centrados 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 entiendan qué los elementos van juntos

Cuando aplicas un modificador clickable a un elemento componible, Compose fusiona automáticamente todos los elementos que contiene el elemento componible. Esto también ocurre ListItem de elementos dentro de un elemento de lista se combinan y la accesibilidad servicios los ven 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 la accesibilidad servicios para verlas como un elemento. Por ejemplo, imagina un elemento componible que muestra el avatar del usuario, su nombre y algunos datos adicionales:

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

Puedes permitir que Compose combine estos elementos con mergeDescendants. en el modificador semantics. De esta manera, los servicios de accesibilidad seleccionar solo el elemento combinado y todas las propiedades semánticas de los elementos subordinados se fusionan.

@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 todo el contenedor a la vez y se 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 en la pantalla, primero selecciona todo el elemento y luego el ícono de marcador.

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 permita a un usuario agregar el elemento a favoritos Recuerda también debes quitar de forma explícita el comportamiento del ícono de favoritos para asegurarse de que no lo seleccione el servicio de accesibilidad. Esta se completa con 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 { }
        )
    }
}

Describe el estado de un elemento

Un elemento componible puede definir un stateDescription para la semántica que El framework de Android usa para leer el estado en el que se encuentra el elemento componible. Para ejemplo, un elemento componible que se puede activar o desactivar puede estar en un estado o un estado "desmarcado" para cada estado. En algunos casos, es posible que desees anular la descripción del estado predeterminado las etiquetas que usa Compose. Para hacerlo, especifica de forma explícita el estado etiquetas de descripción 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 ocasiones, 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 esta pantalla. Para ayudar la navegación, indican qué elementos son encabezados. En el ejemplo anterior, cada el título de la subsección podría definirse como un encabezado para la accesibilidad. Algunos servicios de accesibilidad, como TalkBack, les permiten a los usuarios navegar directamente de un encabezado a otro.

En Compose, para indicar que un elemento componible es un encabezado, se define su semantics:

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

Cómo controlar elementos componibles personalizados

Cuando reemplaces ciertos componentes de Material en tu app con debes tener en cuenta las consideraciones de accesibilidad.

Supongamos que reemplazarás 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 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.

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

Recursos adicionales