Слияние и очистка

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

Семантика объединения

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

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

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

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

Объединенное дерево содержит несколько текстов в списке внутри узла «Строка». Необъединенное дерево содержит отдельные узлы для каждого составного текста.
Рисунок 4. Объединенное дерево содержит несколько текстов в списке внутри узла Row . Необъединенное дерево содержит отдельные узлы для каждого составного Text .

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

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

Четкая и заданная семантика

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

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

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

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

В некоторых сценариях элементы не нужно отправлять в службы доступности — возможно, их дополнительная информация избыточна для доступности или она носит чисто визуально декоративный и неинтерактивный характер. В этих случаях вы можете скрыть элементы с помощью 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{} с пустой лямбдой, когда необходимо очистить родительскую и дочернюю семантику для всех служб.
    • ИспользуйтеclearAndSetSemantics clearAndSetSemantics{/*content*/} с содержимым внутри лямбда-выражения, когда семантику компонента необходимо настроить вручную.
  • Когда контент следует рассматривать как единое целое и требуется, чтобы вся его дочерняя информация была полной:
    • Используйте семантические потомки слияния.
Таблица с различными вариантами использования API.
Рисунок 5. Таблица с различными вариантами использования API.
{% дословно %} {% дословно %} {% дословно %} {% дословно %}