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

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

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

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

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

Например, представьте себе 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)
    }
}

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

Объединенное дерево содержит несколько текстов в списке внутри узла Row. В несведенном дереве для каждого объекта Text composable есть отдельный узел.
Рисунок 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