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 private 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 private 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 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 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 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) ) } }
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 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. 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 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, puedes configurar la etiqueta de clics 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 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:
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 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 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 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 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:
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:
Agrega acciones personalizadas
Observa el siguiente elemento de la lista:
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.
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 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 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:
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.headlineSmall, modifier = Modifier.semantics { heading() } ) }
Pruebas automatizadas de las propiedades de accesibilidad
Si personalizas las propiedades semánticas de tu app, por ejemplo, si sigues los casos de uso mencionados anteriormente, puedes verificar la precisión y evitar regresiones mediante pruebas automatizadas de la IU.
Por ejemplo, para probar que la etiqueta de clic de un elemento esté configurada correctamente, usa el siguiente código:
@Test fun test() { composeTestRule .onNode(nodeMatcher) .assert( SemanticsMatcher("onClickLabel is set correctly") { it.config.getOrNull(SemanticsActions.OnClick)?.label == "My Click Label" } ) }
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.
Modifica el orden de recorrido con isTraversalGroup
y traversalIndex
De forma predeterminada, el comportamiento del lector de pantalla de accesibilidad en una app de Compose se implementa en el orden de lectura esperado, que suele ser de izquierda a derecha y, luego, de arriba a abajo.
Sin embargo, hay algunos tipos de diseños de apps en los que el algoritmo no puede determinar el orden de lectura real sin sugerencias adicionales. En las apps basadas en View, puedes solucionar estos problemas mediante las propiedades traversalBefore
y traversalAfter
.
A partir de Compose 1.5, Compose proporciona una API igual de flexible, pero con un modelo conceptual nuevo.
isTraversalGroup
y traversalIndex
son propiedades semánticas que te permiten controlar la accesibilidad y el orden del enfoque de TalkBack en situaciones en las que el algoritmo de ordenamiento predeterminado no es apropiado. isTraversalGroup
identifica grupos con importancia semántica, mientras que traversalIndex
ajusta el orden de los elementos individuales dentro de esos grupos. Puedes usar isTraversalGroup
solo o con traversalIndex
para una mayor personalización.
En esta página, se describe cómo usar isTraversalGroup
y traversalIndex
en tu app para controlar el orden de recorrido del lector de pantalla.
Agrupa elementos con isTraversalGroup
isTraversalGroup
es una propiedad booleana que define si un nodo de semántica es un grupo de recorrido. Este tipo de nodo es aquel cuya función es servir como límite o borde en la organización de sus elementos secundarios.
Si configuras isTraversalGroup = true
en un nodo, se visitarán todos los elementos secundarios de ese nodo antes de moverse a otros elementos. Puedes configurar isTraversalGroup
en nodos enfocables que no sean del lector de pantalla, como Columnas, Filas o Cuadros.
En este ejemplo, se modifica un fragmento para usar isTraversalGroup
. El siguiente fragmento emite cuatro elementos de texto. Los dos elementos de la izquierda pertenecen a un elemento CardBox
, mientras que los dos de la derecha pertenecen a otro elemento CardBox
:
// CardBox() function takes in top and bottom sample text. @Composable fun CardBox( topSampleText: String, bottomSampleText: String, modifier: Modifier = Modifier ) { Box(modifier) { Column { Text(topSampleText) Text(bottomSampleText) } } } @Composable fun TraversalGroupDemo() { val topSampleText1 = "This sentence is in " val bottomSampleText1 = "the left column." val topSampleText2 = "This sentence is " val bottomSampleText2 = "on the right." Row { CardBox( topSampleText1, bottomSampleText1 ) CardBox( topSampleText2, bottomSampleText2 ) } }
El código produce un resultado similar al siguiente:
Debido a que no se estableció ninguna semántica, el comportamiento predeterminado del lector de pantalla es desviar los elementos de izquierda a derecha y de arriba a abajo. Debido a este valor predeterminado, TalkBack lee los fragmentos de oraciones en el orden incorrecto:
"Esta oración se encuentra en" → "Esta oración es" → "la columna izquierda". → "a la derecha."
Para ordenar los fragmentos correctamente, modifica el fragmento original para establecer isTraversalGroup
en true
:
@Composable fun TraversalGroupDemo2() { val topSampleText1 = "This sentence is in " val bottomSampleText1 = "the left column." val topSampleText2 = "This sentence is" val bottomSampleText2 = "on the right." Row { CardBox( // 1, topSampleText1, bottomSampleText1, Modifier.semantics { isTraversalGroup = true } ) CardBox( // 2, topSampleText2, bottomSampleText2, Modifier.semantics { isTraversalGroup = true } ) } }
Debido a que isTraversalGroup
se establece específicamente en cada CardBox
, los límites de CardBox
se respetan cuando se ordenan sus elementos. En este caso, el CardBox
izquierdo se lee primero, seguido del CardBox
derecho.
Ahora TalkBack lee los fragmentos de las oraciones en el orden correcto:
"Esta oración está en" → "la columna izquierda". → "Esta oración es" → "a la derecha".
Personaliza aún más el orden de recorrido con traversalIndex
traversalIndex
es una propiedad flotante que te permite personalizar el orden de recorrido de TalkBack. Si no basta con agrupar los elementos para que TalkBack funcione correctamente, puedes usar traversalIndex
junto con isTraversalGroup
para personalizar aún más el orden del lector de pantalla.
La propiedad traversalIndex
tiene las siguientes características:
- Los elementos con valores de
traversalIndex
más bajos tienen prioridad. - Puede ser positivo o negativo.
- El valor predeterminado es
0f
. - Solo afecta a los nodos enfocables del lector de pantalla, como los elementos en pantalla, como Texto o Botones. Por ejemplo, configurar solo
traversalIndex
en una columna no tendría ningún efecto, a menos que la columna también tenga un valor deisTraversalGroup
configurado.
En el siguiente ejemplo, se muestra cómo puedes usar traversalIndex
y isTraversalGroup
juntos.
Ejemplo: formato de reloj transversal
Una cara de reloj es una situación común en la que el ordenamiento de recorrido estándar no funciona. El ejemplo de esta sección se basa en un selector de hora, en el que un usuario puede recorrer los números de una cara de reloj y seleccionar dígitos para las franjas horarias y los minutos.
En el siguiente fragmento simplificado, hay un CircularLayout
en el que se dibujan 12 números, que comienzan con 12 y se mueven en el sentido de las manecillas del reloj alrededor del círculo:
@Composable fun ClockFaceDemo() { CircularLayout { repeat(12) { hour -> ClockText(hour) } } } @Composable private fun ClockText(value: Int) { Box(modifier = Modifier) { Text((if (value == 0) 12 else value).toString()) } }
Debido a que la cara de reloj no se lee de forma lógica con el orden predeterminado de izquierda a derecha y de arriba a abajo, TalkBack lee los números desordenados. Para rectificar esto, usa el valor del contador incremental, como se muestra en el siguiente fragmento:
@Composable fun ClockFaceDemo() { CircularLayout(Modifier.semantics { isTraversalGroup = true }) { repeat(12) { hour -> ClockText(hour) } } } @Composable private fun ClockText(value: Int) { Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) { Text((if (value == 0) 12 else value).toString()) } }
Para establecer correctamente el orden de recorrido, primero haz que CircularLayout
sea un grupo de recorrido y configura isTraversalGroup = true
. Luego, a medida que se dibuja el texto de cada reloj en el diseño, establece su traversalIndex
correspondiente en el valor del contador.
Debido a que el valor del contador aumenta de forma continua, el traversalIndex
de cada valor de reloj es mayor a medida que se agregan números a la pantalla: el valor del reloj 0 tiene un traversalIndex
de 0, el valor del reloj 1 tiene un traversalIndex
de 1, y así sucesivamente. De esta forma, se establece el orden en el que TalkBack los leerá. Ahora, los números dentro de CircularLayout
se leen en el orden esperado.
Debido a que los traversalIndexes
que se establecieron solo son relativos a otros índices dentro de la misma agrupación, se conservó el resto del orden de la pantalla. En otras palabras, los cambios semánticos que se muestran en el fragmento de código anterior solo modifican el orden dentro de la cara de reloj que tiene isTraversalGroup =
true
configurado.
Ten en cuenta que, sin establecer la semántica de CircularLayout's
en isTraversalGroup =
true
, aún se aplican los cambios de traversalIndex
. Sin embargo, sin el CircularLayout
para vincularlos, los doce dígitos de la cara de reloj se leen en último lugar, después de visitar todos los demás elementos de la pantalla. Esto ocurre porque todos los demás elementos tienen un traversalIndex
predeterminado de 0f
, y los elementos de texto del reloj se leen después de todos los demás elementos 0f
.
Ejemplo: Personaliza el orden de recorrido para el botón de acción flotante
En este ejemplo, se usa traversalIndex
y isTraversalGroup
para controlar el orden de recorrido de un botón de acción flotante (BAF) de Material Design. Este ejemplo se basa en el siguiente diseño:
De forma predeterminada, el diseño anterior tiene el siguiente orden de TalkBack:
Barra superior de la app → Textos de ejemplo del 0 al 6 → Botón de acción flotante (BAF) → Barra de la app inferior
Es posible que desees que el lector de pantalla se enfoque primero en el BAF. Para establecer un traversalIndex
en un elemento de Material, como un BAF, haz lo siguiente:
@Composable fun FloatingBox() { Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) { FloatingActionButton(onClick = {}) { Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon") } } }
En este fragmento, crear un cuadro con isTraversalGroup
establecido en true
y configurar un traversalIndex
en el mismo cuadro (-1f
es menor que el valor predeterminado de 0f
) significa que el cuadro flotante se coloca antes de todos los demás elementos en pantalla.
A continuación, puedes colocar el cuadro flotante y otros elementos en un andamiaje, que implementa un diseño simple de Material Design:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ColumnWithFABFirstDemo() { Scaffold( topBar = { TopAppBar(title = { Text("Top App Bar") }) }, floatingActionButtonPosition = FabPosition.End, floatingActionButton = { FloatingBox() }, content = { padding -> ContentColumn(padding = padding) }, bottomBar = { BottomAppBar { Text("Bottom App Bar") } } ) }
TalkBack interactúa con los elementos en el siguiente orden:
BAF → Barra superior de la app → Textos de ejemplo del 0 al 6 → Barra inferior de la app
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.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Semántica en Compose
- Información sobre los gestos
- Material Design 2 en Compose