Управлять порядком обхода

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

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

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

Группировать элементы с помощью isTraversalGroup

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

Установка 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 для столбца не будет иметь никакого эффекта, если для столбца также не установлен 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 .

Пример: настройка порядка обхода для кнопки плавающего действия

В этом примере traversalIndex и isTraversalGroup управляют порядком обхода кнопки плавающего действия Material Design (FAB). Основой этого примера является следующая компоновка:

Макет с верхней панелью приложения, образцом текста, плавающей кнопкой действия и нижней панелью приложения.
Рис. 3. Макет с верхней панелью приложения, примером текста, плавающей кнопкой действия и нижней панелью приложения.

По умолчанию макет в этом примере имеет следующий порядок TalkBack:

Верхняя панель приложений → Примеры текстов от 0 до 6 → кнопка плавающего действия (FAB) → Нижняя панель приложений.

Возможно, вы захотите, чтобы программа чтения с экрана сначала сосредоточилась на FAB. Чтобы установить traversalIndex для элемента Material, такого как FAB, выполните следующие действия:

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

В этом фрагменте создание поля с isTraversalGroup для которого установлено значение true , и установка traversalIndex для того же поля ( -1f меньше значения по умолчанию, равного 0f ), означает, что плавающее поле появляется перед всеми другими элементами на экране.

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

FAB → Верхняя панель приложений → Примеры текстов от 0 до 6 → Нижняя панель приложений.

Дополнительные ресурсы