Controla el orden de recorrido

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 vistas, puedes solucionar estos problemas con 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.

Usa 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 los elementos secundarios del nodo.

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 el siguiente ejemplo, se usa isTraversalGroup. 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:

Diseño con dos columnas de texto, en la que la columna de la izquierda dice "Esta oración está en la columna izquierda" y la columna de la derecha dice "Esta oración está a la derecha".
Figura 1: Un diseño con dos oraciones (una en la columna izquierda y otra en la columna derecha).

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 aplican 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".

Personalizar más el orden de recorrido

traversalIndex es una propiedad flotante que te permite personalizar el orden de recorrido de TalkBack. Si agrupar elementos no es suficiente para que TalkBack funcione correctamente, usa traversalIndex junto con isTraversalGroup para personalizar aún más el orden de los lectores 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 el texto o los botones. Por ejemplo, configurar solo traversalIndex en una columna no tendría ningún efecto, a menos que la columna también tenga un isTraversalGroup 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 es 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.

Una cara de reloj con un selector de hora sobre ella.
Figura 2: La imagen de una cara de reloj

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, y el valor del reloj 1 tiene un traversalIndex de 1). De esta manera, se establece el orden en el que TalkBack las lee. Ahora, los números dentro de CircularLayout se leen en el orden esperado.

Debido a que los traversalIndexes que se configuraron 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 establecido.

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, traversalIndex y isTraversalGroup controlan el orden de recorrido de un botón de acción flotante (BAF) de Material Design. La base de este ejemplo es el siguiente diseño:

Diseño con una barra superior de la app, texto de ejemplo, un botón de acción flotante y una barra inferior de la app
Figura 3: Diseño con una barra superior de la app, texto de ejemplo, un botón de acción flotante y una barra inferior de la app

De forma predeterminada, el diseño de este ejemplo 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 que 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 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

Recursos adicionales