Объединение и удаление

Когда сервисы специальных возможностей перемещаются по элементам на экране, важно, чтобы эти элементы были сгруппированы, разделены или даже скрыты с нужной степенью детализации. Когда каждая отдельная низкоуровневая composable-функция на экране выделяется независимо, пользователям приходится много взаимодействовать, чтобы перемещаться по экрану. Если элементы объединяются слишком агрессивно, пользователи могут не понять, какие элементы логически принадлежат друг другу. Если на экране есть элементы, которые являются чисто декоративными, их можно скрыть от специальных возможностей. В таких случаях вы можете использовать API Compose для объединения, очистки и скрытия семантики.

Объединить семантику

Когда вы применяете модификатор clickable к родительской composable-функции, Compose автоматически объединяет все дочерние элементы. Чтобы узнать, как интерактивные компоненты Compose Material и Foundation по умолчанию используют стратегии объединения, ознакомьтесь с разделом Интерактивные элементы.

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

Например, рассмотрим composable-функцию, которая показывает аватар пользователя, его имя и некоторую дополнительную информацию:

Группа элементов интерфейса, включая имя пользователя. Выбрано название.
Рисунок 1. Группа элементов интерфейса, включая имя пользователя. Название выбрано.

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

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

Теперь сервисы специальных возможностей фокусируются на всем контейнере сразу и объединяют его содержимое:

Группа элементов интерфейса, включая имя пользователя. Все элементы выбираются вместе.
Рисунок 2. Группа элементов интерфейса, включая имя пользователя. Все элементы выбираются вместе.

Для каждого свойства семантики определена стратегия объединения. Например, свойство ContentDescription добавляет в список все дочерние значения ContentDescription. Вы можете проверить стратегию объединения свойства семантики, посмотрев его реализацию mergePolicy в SemanticsProperties.kt. Свойства могут принимать значение родительского или дочернего элемента, объединять значения в список или строку, вообще не допускать объединения и вместо этого вызывать исключение или любую другую пользовательскую стратегию объединения.

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

Элемент списка с изображением, текстом и значком закладки
Рисунок 3. Элемент списка с изображением, текстом и значком закладки.

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

Когда пользователь нажимает на элемент clickableRow, открывается статья. Внутри него находится элемент BookmarkButton, позволяющий добавить статью в закладки. Эта вложенная кнопка отображается как не объединенная, а остальной дочерний контент в строке объединен:

Объединенное дерево содержит несколько текстов в списке внутри узла Row. Необъединенное дерево содержит отдельные узлы для каждого составного элемента Text.
Рисунок 4. Объединенное дерево содержит несколько текстов в списке внутри узла Row. В неслитом дереве есть отдельные узлы для каждой Text composable-функции
.

Некоторые composable-функции не объединяются автоматически в родительский элемент. Родительский элемент не может объединять дочерние, если они также объединяются, например когда для них задано свойство mergeDescendants = true или они являются компонентами, которые объединяются сами по себе (кнопки или интерактивные элементы). Знание того, как определенные API объединяются или не объединяются, может помочь вам отладить некоторые потенциально неожиданные поведения.

Объединяйте дочерние элементы, если они составляют логическую группу. Но если вложенные дочерние элементы требуют ручной настройки или удаления собственной семантики, другие API могут лучше соответствовать вашим потребностям (например, clearAndSetSemantics).

Очистить и задать семантику

Если семантическую информацию нужно полностью очистить или перезаписать, используйте мощный API clearAndSetSemantics.

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

Обратите внимание, что при очистке с помощью пустой лямбда-функции очищенная семантика не отправляется ни одному потребителю, который использует эту информацию, например для специальных возможностей, автозаполнения или тестирования. При перезаписи контента с помощью clearAndSetSemantics{/*semantic information*/} новая семантика заменяет всю предыдущую семантику элемента и его потомков.

Ниже приведен пример специального компонента переключателя, представленного интерактивной строкой со значком и текстом:

// 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?")
    }
}

Хотя значок и текст содержат некоторую семантическую информацию, вместе они не указывают на то, что этот компонент является переключаемым. Объединения недостаточно, поскольку вам необходимо предоставить дополнительную информацию о компоненте.

Поскольку приведенный выше фрагмент создает пользовательский компонент переключателя, вам нужно добавить возможность переключения, а также семантику stateDescription, toggleableState и role. В этом случае статус компонента и связанное с ним действие будут доступны. Например, TalkBack озвучит "Нажмите дважды, чтобы переключить" вместо "Нажмите дважды, чтобы активировать".

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

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

  • Поскольку при использовании этого API сервисы не получают никакой информации, лучше использовать его с осторожностью.
    • Семантическая информация может использоваться ИИ-агентами и другими сервисами для понимания экрана, поэтому ее следует удалять только при необходимости.
  • Специальную семантику можно задать в лямбда-выражении API.
  • Порядок модификаторов имеет значение – этот API очищает всю семантику, которая находится после места его применения, независимо от других стратегий слияния.

Скрыть семантику

В некоторых случаях элементы не нужно отправлять в сервисы специальных возможностей. Например, если дополнительная информация не нужна для специальных возможностей или если элемент является чисто декоративным и неинтерактивным. В таких случаях вы можете скрыть элементы с помощью API hideFromAccessibility.

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

@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 •"
    )
}

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

Распределение по вариантам использования

Ниже приведены примеры использования, которые помогут вам понять, чем отличаются предыдущие API.

  • Если контент не предназначен для использования сервисами специальных возможностей:
    • Используйте hideFromAccessibility, если контент может быть декоративным или избыточным, но его все равно нужно протестировать.
    • Используйте clearAndSetSemantics{} с пустым лямбда-выражением, когда нужно очистить семантику родительских и дочерних элементов для всех сервисов.
    • Используйте clearAndSetSemantics{/*content*/} с контентом внутри лямбда-выражения, когда семантику компонента нужно задать вручную.
  • Если контент должен обрабатываться как единое целое и для его полноты требуется информация обо всех дочерних элементах:
    • Используйте объединение семантических потомков.
Таблица с различными вариантами использования API.
Рисунок 5. Таблица с различными вариантами использования API.