Semántica

Además de la información principal que lleva un elemento componible, como una cadena de texto de un elemento componible Text, puede ser útil tener más información complementaria sobre los elementos de la IU.

La información sobre el significado y el rol de un componente en Compose se denomina semántica, que es una forma de proporcionar contexto adicional sobre los elementos componibles a servicios como la accesibilidad, el autocompletado y las pruebas. Por ejemplo, un ícono de cámara podría ser solo una imagen, pero el significado semántico podría ser "Tomar una foto".

Cuando combinas la semántica adecuada con las APIs de Compose adecuadas, proporcionas la mayor cantidad de información posible sobre tu componente a los servicios de accesibilidad, que luego deciden cómo representarlo al usuario.

Las APIs de Material y Compose de la IU y Foundation incluyen semánticas integradas que siguen su rol y función específicos, pero también puedes modificar estas semánticas para las APIs existentes o establecer otras nuevas para componentes personalizados, según tus requisitos específicos.

Propiedades semánticas

Las propiedades semánticas transmiten el significado del elemento correspondiente que admite composición. Por ejemplo, el elemento Text que admite composición incluye una propiedad semántica text, ya que ese es el significado de ese tipo de elemento. Un objeto Icon tiene una propiedad contentDescription (si lo establece el desarrollador) que transmite, por medio de texto, cuál es el significado del ícono.

Considera cómo las propiedades semánticas transmiten el significado de un elemento componible. Considera un Switch. El usuario lo verá de la siguiente manera:

Figura 1: Un Switch en sus estados "Activado" y "Desactivado".

Para describir el significado de este elemento, puedes decir lo siguiente: "Este es un interruptor, el cual es un elemento que se puede activar o desactivar, y, en este momento, se encuentra en estado "Activado". Puedes hacer clic en él para interactuar con él".

Las propiedades semánticas se usan exactamente para este fin. El nodo semántico de este elemento Switch incluye las siguientes propiedades, como se visualizan con el Inspector de diseño:

Inspector de diseño que muestra las propiedades semánticas de un elemento Switch componible
Figura 2: El Inspector de diseño muestra las propiedades semánticas de un elemento componible Switch.

El Role indica el tipo de elemento. El elemento StateDescription describe cómo se debe hacer referencia al estado "Activado". De forma predeterminada, esta es una versión localizada de la palabra "Activado", pero puede ser más específica (por ejemplo, "Habilitado") según el contexto. El ToggleableState es el estado actual del interruptor. La propiedad OnClick hace referencia al método que se utiliza para interactuar con este elemento.

Hacer un seguimiento de las propiedades semánticas de cada elemento que admite composición en la app ofrece muchas posibilidades potentes:

  • Los servicios de accesibilidad usan las propiedades para representar la IU que se muestra en la pantalla y permitir que los usuarios interactúen con ella. Para el elemento componible Switch, TalkBack podría anunciar lo siguiente: "Activado; interruptor; presiona dos veces para activar o desactivar". El usuario puede presionar dos veces la pantalla para desactivar el interruptor.
  • El framework de pruebas usa las propiedades para encontrar nodos, interactuar con ellos y realizar aserciones:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Los elementos que admiten composición y los modificadores que se compilan sobre la biblioteca base de Compose ya establecen las propiedades relevantes por ti de forma predeterminada. De manera opcional, puedes cambiar estas propiedades de forma manual para mejorar la compatibilidad con la accesibilidad en casos de uso específicos o cambiar la estrategia de combinación o limpieza de tus elementos componibles.

Para indicar el tipo de contenido específico de tu componente a los servicios de accesibilidad, puedes aplicar una variedad de semánticas diferentes. Estas incorporaciones admitirán la información semántica principal y ayudarán a los servicios de accesibilidad a ajustar cómo se representa, anuncia o interactúa con tu componente.

Para obtener una lista completa de las propiedades semánticas, consulta el objeto SemanticsProperties. Para obtener una lista completa de las acciones de accesibilidad posibles, consulta el objeto SemanticsActions.

Encabezados

Las apps suelen contener pantallas con contenido con mucho texto, como artículos largos o páginas de noticias, que suelen dividirse en diferentes sub secciones con encabezados:

Una entrada de blog con el texto del artículo en un contenedor desplazable.
Figura 3: Una entrada de blog con el texto del artículo en un contenedor desplazable.

Los usuarios con necesidades de accesibilidad pueden tener dificultades para navegar por una pantalla de este tipo con facilidad. Para mejorar la experiencia de navegación, algunos servicios de accesibilidad permiten navegar más fácilmente directamente entre secciones o encabezados. Para habilitar esto, indica que tu componente es un heading definiendo su propiedad semántica:

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

Alertas y ventanas emergentes

Si tu componente es una alerta o una ventana emergente, como una Snackbar, te recomendamos que indiques a los servicios de accesibilidad que se puede transmitir a los usuarios una estructura nueva o actualizaciones del contenido.

Los componentes similares a alertas se pueden marcar con la propiedad de semántica liveRegion. Esto permite que los servicios de accesibilidad notifiquen automáticamente al usuario sobre los cambios en este componente o en sus elementos secundarios:

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

Debes usar liveRegionMode.Polite en la mayoría de los casos en los que la atención de los usuarios solo debe enfocarse brevemente en alertas o contenido importante que cambia en la pantalla.

Debes usar liveRegion.Assertive con moderación para evitar comentarios disruptivos. Se debe usar en situaciones en las que es fundamental que los usuarios conozcan el contenido urgente:

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

No se deben usar regiones activas en contenido que se actualiza con frecuencia, como los temporizadores de cuenta regresiva, para evitar abrumar a los usuarios con comentarios constantes.

Componentes similares a ventanas

Los componentes personalizados similares a ventanas, como ModalBottomSheet, necesitan indicadores adicionales para diferenciarlos del contenido circundante. Para ello, puedes usar la semántica paneTitle, de modo que los servicios de accesibilidad puedan representar de forma adecuada cualquier cambio relevante en la ventana o el panel, junto con su información semántica principal:

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

Como referencia, consulta cómo Material 3 usa paneTitle para sus componentes.

Componentes de errores

Para otros tipos de contenido, como los componentes similares a errores, te recomendamos que expandas la información semántica principal para los usuarios con necesidades de accesibilidad. Cuando definas los estados de error, puedes informar a los servicios de accesibilidad su semántica de error y proporcionar mensajes de error expandidos.

En este ejemplo, TalkBack lee la información principal del texto de error, seguida de mensajes adicionales expandidos:

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

Componentes de seguimiento del progreso

En el caso de los componentes personalizados que hacen un seguimiento del progreso, te recomendamos que notifiques a los usuarios sus cambios, incluido el valor actual, el rango y el tamaño del paso. Puedes hacerlo con la semántica de progressBarRangeInfo, lo que garantiza que los servicios de accesibilidad estén al tanto de los cambios de progreso y puedan actualizar a los usuarios según corresponda. Las diferentes tecnologías de accesibilidad también pueden tener formas únicas de sugerir un aumento o una disminución de la progresión.

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

Información de la lista y del artículo

En las listas y cuadrículas personalizadas con muchos elementos, puede ser útil que los servicios de accesibilidad también reciban información más detallada, como la cantidad total de elementos y índices.

Cuando se usan las semánticas collectionInfo y collectionItemInfo en la lista y los elementos, respectivamente, en esta lista larga, los servicios de accesibilidad pueden informar a los usuarios en qué índice de elementos se encuentran de la colección total, además de la información semántica textual:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

Descripción del estado

Un elemento componible puede definir una stateDescription para la semántica que usa el framework de Android para leer el estado en el que se encuentra 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, 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 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() }
            )
    ) {
        /* ... */
    }
}

Acciones personalizadas

Las acciones personalizadas se pueden usar para gestos de pantalla táctil más complejos, como deslizar para descartar o arrastrar y soltar, ya que pueden ser un desafío para que los usuarios con discapacidades motoras o de otro tipo interactúen con ellos.

Para que el gesto de deslizar para descartar sea más accesible, puedes vincularlo a una acción personalizada, pasar la acción de descarte y etiquetarla allí:

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

Luego, un servicio de accesibilidad como TalkBack destaca el componente y sugiere que hay más acciones disponibles en su menú, lo que representa la acción de deslizar para descartar:

Visualización del menú de acciones de TalkBack
Figura 4: Visualización del menú de acciones de TalkBack.

Otro caso de uso para las acciones personalizadas son las listas largas con elementos que tienen más acciones disponibles, ya que podría ser tedioso para los usuarios iterar por cada acción de cada elemento por separado:

=Visualización de la navegación de la Accesibilidad con interruptores en pantalla
Figura 5: Visualización de la navegación de la Accesibilidad con interruptores en pantalla.

Para mejorar la experiencia de navegación, que es especialmente útil para las tecnologías de accesibilidad basadas en la interacción, como Accesibilidad con interruptores o Accesibilidad por voz, puedes usar acciones personalizadas en el contenedor para mover acciones del recorrido individual a un menú de acciones independiente:

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

En estos casos, asegúrate de borrar manualmente la semántica de los elementos secundarios originales con el modificador clearAndSetSemantics, ya que los trasladarás a acciones personalizadas.

Tomando como ejemplo el Accesibilidad con interruptores, su menú se abre cuando se selecciona el contenedor y muestra las acciones anidadas disponibles:

Elemento destacado de Accesibilidad con interruptores del artículo de la lista
Figura 6: Accesibilidad con interruptores destacada del elemento de la lista de artículos.
Visualización del menú de acciones de Accesibilidad con interruptores.
Figura 7: Visualización del menú de acciones de Accesibilidad con interruptores.

Árbol semántico

Una composición describe la IU de la app y se produce mediante la ejecución de elementos componibles. La composición es una estructura de árbol que consta de elementos que admiten composición y que describen la IU.

Junto a la composición, existe un árbol paralelo, que se denomina árbol semántico. En este árbol, se describe la IU de una manera alternativa que pueden comprender los servicios de accesibilidad y el framework de prueba. Los servicios de accesibilidad usan el árbol para describirles la app a los usuarios con una necesidad específica. El framework de pruebas usa el árbol para interactuar con la app y realizar aserciones sobre esta. El árbol semántico no incluye información para dibujar elementos que admiten composición, pero contiene información sobre el significado semántico de estos elementos.

Una jerarquía de IU típica y su árbol semántico
Figura 8: Una jerarquía de IU típica y su árbol semántico.

Si la app se conforma de elementos que admiten composición y modificadores de la biblioteca base y Material de Compose, el árbol semántico se completará y generará automáticamente. Sin embargo, cuando agregues elementos personalizados que admitan composición de bajo nivel, deberás brindar su semántica de forma manual. También, en algunas situaciones, es posible que el árbol no represente de forma correcta o completa el significado de los elementos en la pantalla. En este caso, puedes adaptar el árbol.

Por ejemplo, ten en cuenta este calendario personalizado que admite composición:

Un calendario personalizado que admite composición con elementos de día seleccionables
Figura 9: Un calendario personalizado que admite composición con elementos de día seleccionables.

En este ejemplo, todo el calendario se implementa como un solo elemento que admite composición de bajo nivel si se usa el objeto Layout que admite composición y se dibuja directamente en Canvas. Si no realizas ninguna otra acción, los servicios de accesibilidad no recibirán suficiente información sobre el contenido del elemento que admite composición y la selección del usuario en el calendario. Por ejemplo, si un usuario hace clic en el día que contiene 17, el marco de trabajo de accesibilidad solamente recibe la información de descripción de todo el control de calendario. En este caso, el servicio de accesibilidad de TalkBack solo anunciaría "Calendario" o la variante "Calendario de abril" (solo ligeramente mejor), y el usuario no sabría qué día se seleccionó. Para que este elemento componible sea más accesible, deberás agregar la información semántica de forma manual.

Árbol combinado y separado

Como se mencionó antes, es posible que cada elemento que admite composición en el árbol de IU no tenga propiedades semánticas establecidas o que sí las tenga. Cuando un elemento que admite composición no tiene propiedades semánticas establecidas, no se incluye como parte del árbol semántico. De esa manera, el árbol semántico solamente incluye los nodos que, en realidad, tienen significado semántico. Sin embargo, con frecuencia, para transmitir el significado correcto de lo que se muestra en la pantalla, también es útil combinar subárboles determinados de nodos y tratarlos como uno solo. De esta manera, puedes pensar en un conjunto de nodos como un todo, en lugar de tratar cada nodo subordinado de forma individual. Como regla general, cada nodo de este árbol representa un elemento enfocable cuando se usan los servicios de accesibilidad.

Un ejemplo de este elemento componible es Button. Puedes razonar sobre un botón como un elemento único, aunque es posible que incluya varios nodos secundarios:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

En el árbol de semántica, se combinan las propiedades de los elementos subordinados del botón, y el botón se presenta como un único nodo de hoja en el árbol:

Representación de semántica de hoja única combinada
Figura 10: Representación de semántica de hoja única combinada.

Los elementos que admiten composición y los modificadores pueden indicar que quieren combinar las propiedades semánticas de sus elementos subordinados mediante una llamada a Modifier.semantics (mergeDescendants = true) {}. Establecer esta propiedad en true indica que se deben combinar las propiedades semánticas. En el ejemplo de Button, el elemento Button que admite composición usa de forma interna el modificador clickable que incluye este modificador semantics. Por lo tanto, se combinan los nodos subordinados del botón. Lee la documentación de accesibilidad para obtener más información sobre cuándo debes cambiar el comportamiento de combinación en el elemento que admite composición.

Varios modificadores y elementos que admiten composición en las bibliotecas base y material de Compose tienen esta propiedad establecida. Por ejemplo, los modificadores clickable y toggleable combinarán automáticamente sus elementos subordinados. El elemento ListItem que admite composición también los combinará.

Inspecciona el árbol

El árbol semántico, en realidad, es dos árboles diferentes. Hay un árbol semántico combinado, que une los nodos subordinados cuando mergeDescendants se establece en true. También hay un árbol semántico separado, que no aplica la combinación, pero mantiene todos los nodos intactos. Los servicios de accesibilidad usan el árbol separado y aplican sus propios algoritmos de combinación en función de la propiedad mergeDescendants. El framework de pruebas usa el árbol combinado de forma predeterminada.

Puedes inspeccionar ambos árboles con el método printToLog(). De forma predeterminada, y como en los ejemplos anteriores, se registra el árbol combinado. Para imprimir el árbol separado, establece el parámetro useUnmergedTree del comparador onRoot() en true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

El Inspector de diseño te permite mostrar el árbol semántico combinado y el separado. Para ello, selecciona el que prefieras en el filtro de vista:

Opciones de vista del Inspector de diseño que permiten mostrar el árbol semántico combinado y el separado
Figura 11: Opciones de vista del Inspector de diseño que permiten mostrar el árbol semántico combinado y el separado.

Para cada nodo del árbol, el Inspector de diseño muestra la semántica combinada y la que se establece en ese nodo en el panel de propiedades:

Propiedades semánticas combinadas y establecidas
Figura 12: Las propiedades semánticas se fusionaron y se establecieron.

De forma predeterminada, los comparadores en el marco de trabajo de prueba usan el árbol semántico combinado. Por este motivo, puedes interactuar con un Button si haces coincidir el texto que se muestra dentro de este:

composeTestRule.onNodeWithText("Like").performClick()

Para anular este comportamiento, configura el parámetro useUnmergedTree de los comparadores en true, como con el comparador onRoot.

Adapta el árbol

Como se mencionó antes, puedes anular o borrar propiedades semánticas determinadas, o cambiar el comportamiento de combinación del árbol. Esto es particularmente relevante cuando creas tus propios componentes personalizados. Sin configurar las propiedades y el comportamiento de combinación correctos, es posible que no se pueda acceder a la app y que las pruebas se comporten de manera diferente a la esperada. Si quieres obtener más información sobre las pruebas, consulta la guía de pruebas.