Các bước chính để cải thiện tính năng hỗ trợ tiếp cận của Compose

Để giúp những người có nhu cầu hỗ trợ tiếp cận sử dụng thành công ứng dụng của bạn, hãy thiết kế để hỗ trợ các yêu cầu chính về hỗ trợ tiếp cận.

Xem xét kích thước đích chạm tối thiểu

Mọi phần tử trên màn hình mà ai đó có thể nhấp, chạm vào hoặc tương tác đều phải đủ lớn để tương tác đáng tin cậy. Khi định kích thước các phần tử này, hãy đảm bảo đặt kích thước tối thiểu thành 48dp để tuân thủ chính xác Material Design nguyên tắc hỗ trợ tiếp cận.

Thành phần Material – như Checkbox, RadioButton, Switch, SliderSurface – đặt kích thước tối thiểu này trong nội bộ, nhưng chỉ khi thành phần đó có thể nhận thao tác của người dùng. Ví dụ: khi Checkbox có tham số onCheckedChange được đặt thành giá trị không rỗng, hộp đánh dấu sẽ bao gồm khoảng đệm có chiều rộng và chiều cao tối thiểu là 48 dp.

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

Khi tham số onCheckedChange được đặt thành rỗng, khoảng đệm sẽ không vì không thể tương tác trực tiếp với thành phần này.

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

Hình 1. Hộp đánh dấu không có khoảng đệm.

Khi triển khai chế độ kiểm soát lựa chọn như Switch, RadioButton hoặc Checkbox, bạn thường nâng hành vi có thể nhấp lên vùng chứa mẹ, đặt lệnh gọi lại nhấp chuột trên thành phần kết hợp vào null rồi thêm toggleable hoặc Đối tượng sửa đổi selectable cho thành phần kết hợp mẹ.

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

Khi kích thước của một thành phần kết hợp có thể nhấp nhỏ hơn kích thước đích chạm tối thiểu, Compose vẫn tăng kích thước đích chạm. Để làm được điều đó, công cụ này mở rộng kích thước đích chạm nằm ngoài ranh giới của thành phần kết hợp.

Ví dụ sau chứa một Box rất nhỏ có thể nhấp vào. Đích chạm diện tích đó sẽ tự động mở rộng ra ngoài phạm vi của Box, vì vậy nhấn bên cạnh Box vẫn kích hoạt sự kiện nhấp chuột.

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

Để ngăn sự chồng chéo có thể có giữa các khu vực cảm ứng của các thành phần kết hợp khác nhau, hãy luôn luôn sử dụng kích thước tối thiểu đủ lớn cho thành phần kết hợp. Trong ví dụ, điều đó sẽ có nghĩa là sử dụng đối tượng sửa đổi sizeIn để đặt kích thước tối thiểu cho hộp bên trong:

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

Thêm nhãn nhấp chuột

Bạn có thể sử dụng nhãn nhấp chuột để thêm ý nghĩa ngữ nghĩa vào hành vi nhấp của một thành phần có thể kết hợp. Nhãn lượt nhấp mô tả những gì sẽ xảy ra khi người dùng tương tác với thành phần kết hợp. Các dịch vụ hỗ trợ tiếp cận sử dụng nhãn lượt nhấp để giúp mô tả ứng dụng người dùng có nhu cầu cụ thể.

Đặt nhãn lượt nhấp bằng cách truyền một tham số vào đối tượng sửa đổi 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
        )
    ) {
        // ..
    }
}

Ngoài ra, nếu bạn không có quyền truy cập vào đối tượng sửa đổi có thể nhấp, hãy đặt nhãn nhấp chuột trong đối tượng sửa đổi ngữ nghĩa:

@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)
        }
    ) {
        // ..
    }
}

Mô tả phần tử hình ảnh

Khi bạn xác định thành phần kết hợp Image hoặc Icon, sẽ không có tự động để khung Android hiểu được ứng dụng là gì đang hiển thị. Bạn cần truyền nội dung mô tả dạng văn bản của phần tử hình ảnh.

Hãy tưởng tượng một màn hình mà người dùng có thể chia sẻ trang hiện tại với bạn bè. Màn hình này chứa biểu tượng chia sẻ có thể nhấp:

Một dải biểu tượng có thể nhấp vào, với

Chỉ dựa vào biểu tượng này, khung Android không thể mô tả nó bằng một hình ảnh trực quan người dùng khiếm khuyết. Khung Android cần có thêm mô tả bằng văn bản về biểu tượng.

Tham số contentDescription mô tả một phần tử hình ảnh. Hãy sử dụng vì URL hiển thị với người dùng.

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

Một số thành phần hình ảnh chỉ mang tính chất trang trí và có thể bạn sẽ không muốn truyền đạt cho người dùng. Khi đặt tham số contentDescription thành null, bạn cho khung Android biết rằng phần tử này không được liên kết hành động hoặc trạng thái.

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

Bạn có toàn quyền quyết định xem một phần tử hình ảnh nhất định có cần có contentDescription hay không. Hãy tự hỏi liệu các phần tử đó có truyền tải thông tin mà người dùng cần thực hiện tác vụ của họ. Nếu không, bạn nên để nội dung mô tả.

Hợp nhất phần tử

Các dịch vụ Hỗ trợ tiếp cận như TalkBack và Tiếp cận bằng công tắc cho phép người dùng di chuyển trọng tâm đến các phần tử trên màn hình. Điều quan trọng là các phần tử phải được tập trung với độ chi tiết phù hợp. Khi mọi thành phần kết hợp cấp thấp trong màn hình của bạn được tập trung độc lập, nên người dùng phải tương tác nhiều để di chuyển trên màn hình. Nếu các phần tử hợp nhất với nhau quá nhiều, người dùng có thể không hiểu các thành phần thuộc về nhau

Khi bạn áp dụng một đối tượng sửa đổi clickable cho một thành phần kết hợp, Compose tự động hợp nhất tất cả các phần tử có trong thành phần kết hợp đó. Điều này cũng đúng với ListItem; các phần tử trong một mục danh sách hợp nhất với nhau và khả năng hỗ trợ tiếp cận xem chúng dưới dạng một phần tử.

Có thể có một tập hợp các thành phần có thể kết hợp tạo thành một nhóm logic, nhưng nhóm đó không thể nhấp vào được hoặc thuộc một mục danh sách. Bạn vẫn muốn hỗ trợ tiếp cận để xem chúng dưới dạng một phần tử. Ví dụ: hãy tưởng tượng một thành phần kết hợp hiển thị hình đại diện, tên của người dùng và một số thông tin bổ sung:

Một nhóm các thành phần trên giao diện người dùng bao gồm tên của người dùng. Tên đã được chọn.

Bạn có thể cho phép Compose hợp nhất các phần tử này bằng cách sử dụng mergeDescendants trong đối tượng sửa đổi semantics. Bằng cách này, các dịch vụ hỗ trợ tiếp cận chỉ chọn phần tử đã hợp nhất và tất cả các thuộc tính ngữ nghĩa của các phần tử con được hợp nhất.

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

Dịch vụ hỗ trợ tiếp cận hiện tập trung vào toàn bộ vùng chứa cùng một lúc, hợp nhất nội dung của chúng:

Một nhóm các thành phần trên giao diện người dùng bao gồm tên của người dùng. Tất cả các phần tử được chọn cùng nhau.

Thêm thao tác tuỳ chỉnh

Hãy xem mục danh sách sau đây:

Một mục danh sách thông thường sẽ bao gồm tiêu đề bài viết, tác giả và biểu tượng dấu trang.

Khi bạn sử dụng một trình đọc màn hình như TalkBack để nghe nội dung hiển thị trên trên màn hình, trước tiên sẽ chọn toàn bộ mục, sau đó chọn biểu tượng dấu trang.

Mục danh sách trong đó tất cả các phần tử được chọn cùng nhau.

Mục danh sách trong đó chỉ có biểu tượng dấu trang được chọn

Trong một danh sách dài, điều này có thể lặp lại rất nhiều lần. Một phương pháp hiệu quả hơn là xác định một thao tác tuỳ chỉnh cho phép người dùng đánh dấu mục đó. Lưu ý bạn cũng phải xoá rõ ràng hành vi của biểu tượng dấu trang để đảm bảo dịch vụ hỗ trợ tiếp cận không chọn nội dung đó. Chiến dịch này được thực hiện bằng đối tượng sửa đổi 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 { }
        )
    }
}

Mô tả trạng thái của một nguyên tố

Một thành phần kết hợp có thể xác định stateDescription cho ngữ nghĩa mà Khung Android dùng để đọc trạng thái của thành phần kết hợp. Để Ví dụ: một thành phần kết hợp bật/tắt được có thể ở dạng "đã đánh dấu" hoặc "đã bỏ chọn" trạng thái. Trong một số trường hợp, bạn có thể muốn ghi đè nội dung mô tả trạng thái mặc định mà Compose sử dụng. Bạn có thể thực hiện điều này bằng cách chỉ định rõ trạng thái nhãn mô tả trước khi xác định một thành phần kết hợp có thể bật/tắt:

@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() }
            )
    ) {
        /* ... */
    }
}

Xác định tiêu đề

Đôi khi, ứng dụng hiển thị nhiều nội dung trên một màn hình trong một vùng chứa có thể cuộn. Ví dụ: một màn hình có thể hiển thị toàn bộ nội dung của một bài viết mà người dùng đang đọc:

Ảnh chụp màn hình một bài đăng trên blog, với nội dung bài viết trong một vùng chứa có thể cuộn.

Người dùng có nhu cầu hỗ trợ tiếp cận sẽ gặp khó khăn khi di chuyển trên màn hình như vậy. Để hỗ trợ điều hướng, cho biết phần tử nào là tiêu đề. Trong ví dụ trước, mỗi tiêu đề tiểu mục có thể được xác định là tiêu đề cho khả năng tiếp cận. Hơi nhiều các dịch vụ hỗ trợ tiếp cận, như TalkBack, cho phép người dùng điều hướng trực tiếp từ đến tiêu đề.

Trong Compose, bạn cho biết thành phần kết hợp là tiêu đề bằng cách xác định Thuộc tính semantics:

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

Xử lý thành phần kết hợp tuỳ chỉnh

Bất cứ khi nào bạn thay thế một số thành phần Material trong ứng dụng bằng thành phần tuỳ chỉnh phiên bản, bạn phải lưu ý đến việc cân nhắc về khả năng hỗ trợ tiếp cận.

Giả sử bạn đang thay thế Material Checkbox bằng phương thức triển khai của riêng mình. Bạn có thể quên thêm đối tượng sửa đổi triStateToggleable để xử lý các thuộc tính hỗ trợ tiếp cận cho thành phần này.

Theo quy tắc chung, hãy xem xét việc triển khai thành phần này trong thư viện Material và bắt chước mọi hành vi hỗ trợ tiếp cận mà bạn có thể tìm thấy. Ngoài ra, hãy tận dụng nhiều thành phần sửa đổi Foundation, chứ không phải các thành sửa đổi ở cấp độ giao diện người dùng, vì các thành phần sửa đổi này bao gồm các tính năng hỗ trợ tiếp cận ngay từ đầu.

Kiểm thử việc triển khai thành phần tuỳ chỉnh bằng nhiều dịch vụ hỗ trợ tiếp cận để xác minh hành vi của dịch vụ đó.

Tài nguyên khác