Как изменить порядок обхода

Порядок обхода – это порядок, в котором службы специальных возможностей перемещаются по элементам интерфейса. В приложении Compose элементы располагаются в порядке чтения, который обычно идет слева направо, а затем сверху вниз. Однако в некоторых случаях Compose может потребоваться дополнительная информация, чтобы определить правильный порядок чтения.

isTraversalGroup и traversalIndex – это семантические свойства, которые позволяют влиять на порядок перехода для сервисов специальных возможностей в случаях, когда алгоритм сортировки по умолчанию в Compose не подходит. isTraversalGroup определяет семантически важные группы, которые необходимо настроить, а traversalIndex изменяет порядок отдельных элементов в этих группах. Вы можете использовать isTraversalGroup отдельно, чтобы указать, что все элементы в группе должны быть выбраны вместе, или с traversalIndex для дальнейшей настройки.

Используйте в приложении элементы isTraversalGroup и traversalIndex, чтобы управлять порядком перехода программы чтения с экрана.

Группировка элементов для навигации

isTraversalGroup – логическое свойство, определяющее, является ли узел semantics группой для перехода. Этот тип узла используется для организации дочерних узлов.

Если для узла задано значение isTraversalGroup = true, это означает, что все дочерние элементы этого узла будут посещены до перехода к другим элементам. Вы можете задать значение isTraversalGroup для узлов, на которые можно переходить без программы чтения с экрана, например для столбцов, строк или полей.

В следующем примере используется метод isTraversalGroup. Он создает четыре текстовых элемента. Два элемента слева относятся к одному элементу CardBox, а два элемента справа – к другому элементу 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
        )
    }
}

Код выдаст результат, похожий на следующий:

Макет с двумя столбцами текста, в левом столбце которого написано "Это предложение находится в левом столбце", а в правом – "Это предложение находится в правом столбце".
Рисунок 1. Макет с двумя предложениями (одно в левом столбце, другое – в правом).

Поскольку семантика не задана, программа чтения с экрана по умолчанию перемещается между элементами слева направо и сверху вниз. Из-за этого значения по умолчанию TalkBack читает фрагменты предложения в неправильном порядке:

"Это предложение находится в" → "Это предложение находится в" → "левом столбце". → "справа".

Чтобы фрагменты были упорядочены правильно, измените исходный фрагмент, задав для атрибута isTraversalGroup значение 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 }
        )
    }
}

Поскольку isTraversalGroup задается для каждого CardBox отдельно, при сортировке элементов учитываются границы CardBox. В этом случае сначала будет прочитан символ слева CardBox, а затем справа CardBox.

Теперь TalkBack читает фрагменты предложения в правильном порядке:

"Это предложение находится" → "в левом столбце". → "Вот предложение:" → "справа."

Как настроить порядок перехода

traversalIndex – это свойство с плавающей точкой, которое позволяет настроить порядок перехода TalkBack. Если группировка элементов не позволяет TalkBack работать корректно, используйте traversalIndex вместе с isTraversalGroup, чтобы дополнительно настроить порядок чтения с экрана.

Свойство traversalIndex имеет следующие характеристики:

  • Элементы с более низким значением traversalIndex имеют более высокий приоритет.
  • Может быть положительным или отрицательным.
  • Значение по умолчанию: 0f .
  • Чтобы индекс перехода влиял на поведение при переходе, его нужно задать для компонента, который можно выбрать и на котором можно сфокусироваться с помощью сервисов специальных возможностей, например для экранного элемента, такого как текст или кнопка.
    • Если включить только traversalIndex, например в Column, это не даст никакого эффекта, если в столбце не задано значение isTraversalGroup.

В следующем примере показано, как использовать traversalIndex и isTraversalGroup вместе.

Циферблат – это типичный пример, когда стандартный порядок перехода не работает. В примере ниже показан выбор времени, где пользователь может перемещаться по цифрам на циферблате и выбирать часы и минуты.

Циферблат с инструментом выбора времени над ним.
Рисунок 2. Изображение циферблата.

В приведенном ниже упрощенном фрагменте кода показан элемент CircularLayout, в котором нарисованы 12 чисел, начиная с 12 и двигаясь по часовой стрелке по кругу:

@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())
    }
}

Поскольку циферблат не читается логически по умолчанию (слева направо и сверху вниз), TalkBack произносит числа в неправильном порядке. Чтобы исправить это, используйте значение счетчика с увеличением, как показано в следующем фрагменте кода:

@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())
    }
}

Чтобы правильно задать порядок перехода, сначала сделайте CircularLayout группой перехода и задайте isTraversalGroup = true. Затем, когда текст часов будет нарисован на макете, задайте для соответствующего элемента traversalIndex значение счетчика.

Поскольку значение счетчика постоянно увеличивается, каждое значение часов traversalIndex становится больше по мере того, как на экране появляются числа. Значение часов 0 имеет traversalIndex 0, а значение часов 1 имеет traversalIndex 1. Таким образом задается порядок, в котором TalkBack будет их озвучивать. Теперь числа внутри CircularLayout будут прочитаны в ожидаемом порядке.

Поскольку значения traversalIndexes заданы только относительно других индексов в той же группе, порядок остальных экранов не изменился. Другими словами, семантические изменения, показанные в приведенном выше фрагменте кода, влияют только на порядок элементов на циферблате, для которого задано значение isTraversalGroup = true.

Обратите внимание, что без установки семантики CircularLayout's на isTraversalGroup = true изменения traversalIndex все равно будут применены. Однако без элемента CircularLayout, который связывает их, двенадцать цифр циферблата будут прочитаны последними, после всех остальных элементов на экране. Это происходит потому, что у всех остальных элементов по умолчанию задано значение traversalIndex, равное 0f, а элементы с текстом часов считываются после всех остальных элементов 0f.

Что необходимо для работы с API

При использовании API для обхода учитывайте следующее:

  • Атрибут isTraversalGroup = true должен быть задан для родительского элемента, содержащего сгруппированные элементы.
  • traversalIndex следует задавать для дочернего компонента, который содержит семантику и будет выбран сервисами специальных возможностей.
  • Убедитесь, что все элементы, которые вы проверяете, находятся на одном уровне zIndex, поскольку это также влияет на семантику и порядок перехода.
  • Убедитесь, что семантика не объединена без необходимости, так как это может повлиять на то, к каким компонентам применяются индексы обхода.