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

Порядок обхода – это последовательность, в которой службы специальных возможностей перемещаются по элементам интерфейса. В приложении 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 зачитывает фрагменты предложения в неправильном порядке:

"This sentence is in" → "This sentence is" → "the left column." → "справа".

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

"Это предложение находится" → "в левом столбце". → "This sentence is" → "on the right."

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

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, так как это также влияет на семантику и порядок обхода.
  • Убедитесь, что семантика не объединяется без необходимости, так как это может повлиять на то, к каким компонентам применяются индексы обхода.