API по умолчанию

API Material, Compose UI и Foundation реализуют и предлагают множество доступных практик по умолчанию. Они содержат встроенную семантику, соответствующую их конкретной роли и функции. Это означает, что поддержка доступности в большинстве случаев обеспечивается практически без дополнительных усилий.

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

Понимание семантики и шаблонов доступности по умолчанию в API Compose поможет вам использовать их с учётом доступности. Это также поможет вам поддерживать доступность в более пользовательских компонентах.

Минимальные размеры сенсорной области

Любой элемент на экране, на который можно нажать, коснуться или с которым можно взаимодействовать, должен быть достаточно большим для обеспечения надёжного взаимодействия. При определении размера этих элементов обязательно установите минимальный размер 48 dp, чтобы обеспечить соответствие рекомендациям Material Design по доступности .

Компоненты Material, такие как Checkbox , RadioButton , Switch , Slider и Surface устанавливают этот минимальный размер внутренне, но только в том случае, если компонент может принимать действия пользователя. Например, если параметр onCheckedChange Checkbox имеет ненулевое значение, флажок включает отступы, чтобы иметь ширину и высоту не менее 48 dp.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

Флажок с отступом по умолчанию шириной и высотой 48 dp.
Рисунок 1. Флажок с отступами по умолчанию.

Если параметр onCheckedChange имеет значение null, заполнение не включается, поскольку с компонентом невозможно взаимодействовать напрямую.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Флажок без отступа.
Рисунок 2. Флажок без отступа.

При реализации элементов управления выбором, таких как Switch , RadioButton или Checkbox , вы обычно переносите поведение нажатия на родительский контейнер, устанавливая обратный вызов click для компонуемого объекта на null и добавляя toggleable или selectable модификатор к родительскому компонуемому объекту.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

Флажок рядом с текстом «Вариант», который выбирается и снимается.
Рисунок 3. Флажок с возможностью нажатия.

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

В следующем примере показан очень маленький интерактивный элемент Box . Область касания автоматически расширяется за пределы Box , поэтому нажатие рядом с Box по-прежнему вызывает событие щелчка.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

Очень маленькое интерактивное поле, которое разворачивается до большего сенсорного объекта при нажатии рядом с полем.
Рисунок 4. Очень маленькое интерактивное поле, которое разворачивается в большую сенсорную область.

Чтобы предотвратить возможное перекрытие сенсорных областей разных компонуемых элементов, всегда используйте достаточно большой минимальный размер для компонуемого элемента. В данном примере это означало бы использование модификатора sizeIn для установки минимального размера внутреннего блока:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

Очень маленький квадрат из предыдущего примера увеличен в размере, чтобы создать большую цель касания.
Рисунок 5. Более крупный квадратный сенсорный объект.

Графические элементы

При определении компонуемого Image или Icon фреймворк Android не может автоматически определить, что именно отображает приложение. Вам необходимо передать текстовое описание графического элемента.

Представьте себе экран, на котором пользователь может поделиться текущей страницей с друзьями. На этом экране есть кликабельный значок «Поделиться»:

Полоса из четырех интерактивных иконок с выделенной иконкой «Поделиться».
Рисунок 6. Ряд кликабельных значков с выбранным значком «Поделиться».

Фреймворк Android не может описать значок пользователю с нарушением зрения, основываясь только на нём. Для этого требуется дополнительное текстовое описание значка.

Параметр contentDescription описывает графический элемент. Используйте локализованную строку, так как она видна пользователю.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Некоторые графические элементы носят исключительно декоративный характер, и вы можете не захотеть сообщать о них пользователю. Устанавливая параметр contentDescription в null , вы сообщаете фреймворку Android, что у этого элемента нет связанных действий или состояний.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

contentDescription в основном предназначен для графических элементов, таких как изображения. Материальные компоненты, такие как Button или Text , а также интерактивные элементы, такие как clickable или toggleable , имеют другие предопределённые семантики, описывающие их внутреннее поведение, и могут быть изменены через другие API Compose.

Интерактивные элементы

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

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

Кнопки с необъединенной и объединенной семантикой дочерних элементов.
Рисунок 7. Кнопки с необъединенной и объединенной семантикой дочерних элементов.

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

Row(
    // Uses `mergeDescendants = true` under the hood
    modifier = Modifier.clickable { openArticle() }
) {
    Icon(
        painter = painterResource(R.drawable.ic_logo),
        contentDescription = "Open",
    )
    Text("Accessibility in Compose")
}

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

Row(
    modifier = Modifier
        .clickable(onClickLabel = "Open this article") {
            openArticle()
        }
) {
    Icon(
        painter = painterResource(R.drawable.ic_logo),
        contentDescription = "Open"
    )
    Text("Accessibility in Compose")
}

Если использовать TalkBack в качестве примера, этот clickable модификатор и его метка нажатия позволят TalkBack выдавать подсказку о действии «Коснитесь дважды, чтобы открыть эту статью», а не более общую обратную связь по умолчанию «Коснитесь дважды, чтобы активировать».

Эта обратная связь меняется в зависимости от типа действия. Длительное нажатие выводит подсказку TalkBack: «Дважды нажмите и удерживайте, чтобы», а затем — подпись:

Row(
    modifier = Modifier
        .combinedClickable(
            onLongClickLabel = "Bookmark this article",
            onLongClick = { addToBookmarks() },
            onClickLabel = "Open this article",
            onClick = { openArticle() },
        )
) {}

В некоторых случаях у вас может не быть прямого доступа к модификатору clickable (например, если он установлен где-то на нижнем вложенном слое), но вы всё равно хотите изменить метку объявления по умолчанию. Для этого разделите установку clickable от изменения объявления с помощью модификатора semantics и установите метку click там же, чтобы изменить представление действия:

@Composable
private fun ArticleList(openArticle: () -> Unit) {
    NestedArticleListItem(
        // Clickable is set separately, in a nested layer:
        onClickAction = openArticle,
        // Semantics are set here:
        modifier = Modifier.semantics {
            onClick(
                label = "Open this article",
                action = {
                    // Not needed here: openArticle()
                    true
                }
            )
        }
    )
}

Вам не нужно передавать действие click дважды. Существующие API Compose, такие как clickable или Button , делают это автоматически. Логика слияния проверяет, что метка и действие самого внешнего модификатора (label) и действие (action) применяются к имеющейся информации. В предыдущем примере NestedArticleListItem автоматически передаёт действие click openArticle() своей семантике clickable . Вы можете оставить действие click null во втором модификаторе семантики action. Однако метка click берётся из второго модификатора семантики onClick(label = "Open this document") поскольку в первом она отсутствовала.

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

Пользовательские компоненты

При создании собственного компонента изучите реализацию аналогичного компонента в библиотеке Material или других библиотеках Compose. Затем при необходимости скопируйте или измените его поведение в плане доступности. Например, если вы замените флажок Material Checkbox собственной реализацией, просмотр существующей реализации Checkbox напомнит вам о необходимости добавить модификатор triStateToggleable , который управляет свойствами доступности компонента. Кроме того, активно используйте модификаторы Foundation, поскольку они учитывают встроенные аспекты доступности и существующие практики Compose, описанные в этом разделе.

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

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}