Ключевые шаги по улучшению доступности Compose

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

Учитывайте минимальные размеры сенсорных целей.

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

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

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

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

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

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

При реализации элементов управления выбором, таких как Switch , RadioButton или Checkbox , вы обычно переносите кликабельное поведение в родительский контейнер, устанавливаете обратный вызов щелчка для составного объекта в значение 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)
        }
    }
}

Если размер интерактивного составного объекта меньше минимального размера сенсорного объекта, 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)
        )
    }
}

Чтобы предотвратить возможное перекрытие сенсорных областей разных составных элементов, всегда используйте достаточно большой минимальный размер для составного объекта. В данном примере это будет означать использование модификатора 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)
        )
    }
}

Добавить метки кликов

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

Установите метку клика, передав параметр в модификаторе clickable :

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

В качестве альтернативы, если у вас нет доступа к модификатору кликабельности, установите метку клика в модификаторе семантики :

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

Опишите визуальные элементы

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

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

Полоса кликабельных значков с

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

Объединить элементы

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

Когда вы применяете clickable модификатор к составному объекту, Compose автоматически объединяет все элементы, содержащиеся в составном объекте. Это также справедливо для ListItem ; элементы внутри элемента списка сливаются вместе, и службы доступности рассматривают их как один элемент.

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

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

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

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

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

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

Добавить специальные действия

Взгляните на следующий элемент списка:

Типичный элемент списка, содержащий название статьи, автора и значок закладки.

Когда вы используете программу чтения с экрана, например Talkback, чтобы прослушать то, что отображается на экране, она сначала выбирает весь элемент, а затем значок закладки.

Элемент списка, в котором все элементы выбраны вместе.

Элемент списка, в котором выбран только значок закладки.

В длинном списке это может стать очень повторяющимся. Лучшим подходом является определение настраиваемого действия, которое позволит пользователю добавить элемент в закладки. Имейте в виду, что вам также необходимо явно удалить поведение самого значка закладки, чтобы убедиться, что он не выбран службой специальных возможностей. Это делается с помощью clearAndSetSemantics :

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

Опишите состояние элемента

Составной объект может определить stateDescription для семантики, которую платформа Android использует для считывания состояния, в котором находится составной объект. Например, переключаемый составной объект может находиться либо в «проверенном», либо в «непроверенном» состоянии. В некоторых случаях вам может потребоваться переопределить метки описания состояния по умолчанию, которые использует Compose. Вы можете сделать это, явно указав метки описания состояния перед определением составного объекта как переключаемого:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Определите заголовки

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

Снимок экрана записи блога с текстом статьи в контейнере с возможностью прокрутки.

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

В Compose вы указываете, что составной элемент является заголовком , определяя его свойство semantics :

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Обработка пользовательских составных элементов

Всякий раз, когда вы заменяете определенные компоненты Material в своем приложении пользовательскими версиями, вы должны учитывать соображения доступности.

Предположим, вы заменяете Checkbox «Материал» своей собственной реализацией. Вы можете забыть добавить модификатор triStateToggleable , который обрабатывает свойства доступности для этого компонента.

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

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

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