Combinación y limpieza

A medida que los servicios de accesibilidad navegan por los elementos de la pantalla, es importante que estos elementos se agrupen, se separen o incluso se oculten con el nivel de detalle correcto. Cuando todos los elementos componibles de bajo nivel de la pantalla se destacan de forma independiente, los usuarios tienen 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. Si hay elementos en la pantalla que son puramente decorativos, estos se pueden ocultar de los servicios de accesibilidad. En estos casos, puedes usar las APIs de Compose para combinar, borrar y ocultar la semántica.

Semántica de combinación

Cuando aplicas un modificador clickable a un elemento componible superior, Compose fusiona automáticamente todos los elementos secundarios debajo de él. Para comprender cómo los componentes interactivos de Material y Foundation de Compose usan estrategias de combinación de forma predeterminada, consulta la sección Elementos interactivos.

Es común que un componente conste de varios elementos componibles. Estos elementos componibles podrían formar un grupo lógico y cada uno podría contener información importante, pero es posible que desees que los servicios de accesibilidad los vean como un solo elemento.

Por ejemplo, piensa en un elemento componible que muestre el avatar de un usuario, su nombre y algunos datos adicionales:

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

Puedes habilitar Compose para que combine estos elementos mediante el parámetro mergeDescendants en el modificador de semántica. De esta manera, los servicios de accesibilidad tratan el componente como una entidad, 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 todo el contenedor de una vez y combinan su contenido:

Grupo de elementos de la IU que incluye el nombre de un usuario. Se seleccionan todos los elementos.
Figura 2: Grupo de elementos de la IU que incluye el nombre de un usuario. Se seleccionan todos los elementos.

Cada propiedad semántica tiene una estrategia de combinación definida. Por ejemplo, la propiedad ContentDescription agrega todos los valores de ContentDescription subordinados a una lista. Puedes comprobar la estrategia de combinación de una propiedad semántica si verificas su implementación mergePolicy en SemanticsProperties.kt. Las propiedades pueden adoptar el valor superior o el secundario, combinar los valores en una lista o una cadena, no permitir la combinación en absoluto y, en su lugar, arrojar una excepción, o cualquier otra estrategia de combinación personalizada.

Hay otras situaciones en las que esperas que la semántica de los elementos secundarios se combine en una superior, pero eso no sucede. En el siguiente ejemplo, tenemos un elemento superior de la lista clickable con elementos secundarios, y podríamos esperar que el elemento superior los combine a todos:

Elemento de lista con imagen, un poco de texto y un ícono de favoritos
Figura 3: Elemento de lista con imagen, un poco de texto y un ícono de favoritos.

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

Cuando el usuario presiona el elemento clickable Row, se abre el artículo. Dentro, hay un BookmarkButton para agregar el artículo a favoritos. Este botón anidado aparece como no fusionado, mientras que el resto del contenido secundario dentro de la fila está fusionado:

El árbol combinado incluye varios textos en una lista dentro del nodo Row El árbol separado incluye nodos que no están combinados para cada elemento Text que admite composición.
Figura 4: El árbol combinado incluye varios textos en una lista dentro del nodo Row. El árbol separado incluye nodos separados para cada elemento Text componible.

Por diseño, algunos elementos componibles no se combinan automáticamente en un elemento superior. Un elemento superior no puede combinar sus elementos secundarios cuando estos también se combinan, ya sea desde la configuración de mergeDescendants = true de forma explícita o a través de componentes que se combinan, como botones o elementos en los que se puede hacer clic. Saber cómo se combinan o se resisten a la combinación ciertas APIs puede ayudarte a depurar algunos comportamientos potencialmente inesperados.

Usa la combinación cuando los elementos secundarios constituyan un grupo lógico y sensato debajo de su elemento superior. Sin embargo, si los elementos secundarios anidados necesitan un ajuste manual o la eliminación de su propia semántica, es posible que otras APIs se adapten mejor a tus necesidades (por ejemplo, clearAndSetSemantics).

Borra y establece semántica

Si la información semántica debe borrarse o reemplazarse por completo, una API potente para usar es clearAndSetSemantics.

Cuando un componente necesita que se borren su propia semántica y la de sus descendientes, usa esta API con una lambda vacía. Cuando se deban reemplazar sus semánticas, incluye tu contenido nuevo dentro de la lambda.

Ten en cuenta que, cuando se borra con una lambda vacía, las semánticas borradas no se envían a ningún consumidor que use esta información, como la accesibilidad, el autocompletado o las pruebas. Cuando se reemplaza el contenido con clearAndSetSemantics{/*semantic information*/}, la semántica nueva reemplaza toda la semántica anterior del elemento y sus subordinados.

El siguiente es un ejemplo de un componente de activación personalizado, representado por una fila interactiva con un ícono y texto:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

Aunque el ícono y el texto tienen cierta información semántica, juntos no indican que este componente sea activable. La combinación no es suficiente, ya que debes proporcionar información adicional sobre el componente.

Como el fragmento anterior crea un componente de activación personalizado, debes agregar la función de activación, así como las semánticas stateDescription, toggleableState y role. De esta manera, el estado del componente y la acción asociada están disponibles. Por ejemplo, TalkBack anuncia "Presiona dos veces para activar o desactivar" en lugar de "Presiona dos veces para activar".

Si borras la semántica original y configuras una nueva y más descriptiva, los servicios de accesibilidad ahora pueden ver que este es un componente que se puede activar o desactivar y que puede alternar el estado.

Cuando uses clearAndSetSemantics, ten en cuenta lo siguiente:

  • Como los servicios no reciben información cuando se configura esta API, es mejor usarla con moderación.
    • Los agentes de IA y los servicios similares pueden usar la información semántica para comprender la pantalla, por lo que solo se debe borrar cuando sea necesario.
  • Se pueden establecer semánticas personalizadas dentro de la expresión lambda de la API.
  • El orden de los modificadores es importante: esta API borra toda la semántica que se encuentra después del lugar en el que se aplica, independientemente de otras estrategias de combinación.

Oculta la semántica

En algunos casos, no es necesario enviar elementos a los servicios de accesibilidad. Es posible que su información adicional sea redundante para la accesibilidad o que sea puramente decorativa y no interactiva. En estos casos, puedes ocultar elementos con la API de hideFromAccessibility.

En los siguientes ejemplos, se muestran componentes que podrían ser necesarios ocultar: una marca de agua redundante que abarca un componente y un carácter que se usa para separar la información de forma decorativa.

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

El uso de hideFromAccessibility aquí garantiza que la marca de agua y la decoración se oculten de los servicios de accesibilidad, pero aún conservan su semántica para otros casos de uso, como las pruebas.

Desglose de los casos de uso

A continuación, se incluye un resumen de casos de uso para comprender cómo diferenciar claramente entre las APIs anteriores:

  • Cuando el contenido no está diseñado para que lo usen los servicios de accesibilidad, haz lo siguiente:
    • Usa hideFromAccessibility cuando el contenido sea posiblemente decorativo o redundante, pero aún debas probarlo.
    • Usa clearAndSetSemantics{} con una lambda vacía cuando se deban borrar las semánticas de elementos superiores y secundarios para todos los servicios.
    • Usa clearAndSetSemantics{/*content*/} con contenido dentro de la expresión lambda cuando se deba configurar manualmente la semántica de un componente.
  • Cuando el contenido debe tratarse como una entidad y necesita que toda la información de sus elementos secundarios esté completa, haz lo siguiente:
    • Usa la combinación de descendientes semánticos.
Tabla con casos de uso de APIs diferenciados.
Figura 5: Tabla con casos de uso diferenciados de la API.