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 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ế ứng dụng để 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 nhớ đặt kích thước tối thiểu là 48 dp để tuân thủ chính xác Nguyên tắc hỗ trợ tiếp cận của Material Design.

Các thành phần Material – chẳng hạn như Checkbox, RadioButton, Switch, SliderSurface – đặt nội bộ kích thước tối thiểu này, nhưng chỉ khi thành phần đó có thể nhận thao tác của người dùng. Ví dụ: khi một Checkbox có thông số onCheckedChange được đặt thành giá trị khác rỗng, hộp đánh dấu sẽ bao gồm các 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 bạn đặt tham số onCheckedChange thành rỗng, khoảng đệm sẽ không được đưa vào 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 các thành phần điều khiển 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 lượt nhấp trên thành phần kết hợp thành null và thêm đối tượng sửa đổi toggleable hoặc selectable vào 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. Thư viện này làm như vậy bằng cách mở rộng kích thước đích chạm bên ngoài ranh giới của thành phần kết hợp.

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

@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 chặn sự trùng lặp có thể giữa các khu vực cảm ứng của nhiều thành phần kết hợp, hãy 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ụ này, điều đó 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 nhấp chuột 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 cho người dùng có nhu cầu cụ thể.

Thiết lập 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 lượt nhấp 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 một thành phần kết hợp Image hoặc Icon, không có cách tự động nào để khung Android hiểu được ứng dụng đang hiển thị nội dung gì. Bạn cần truyền 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, cùng với

Chỉ dựa vào biểu tượng, khung Android sẽ không thể mô tả biểu tượng này cho người dùng khiếm thị. Khung Android cần có thêm một đoạn 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. Sử dụng một chuỗi đã bản địa hoá vì người dùng có thể nhìn thấy chuỗi này.

@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ỉ đơn thuần mang tính chất trang trí và bạn có thể không muốn thông báo cho người dùng. Khi đặt tham số contentDescription thành null, bạn sẽ chỉ báo cho khung Android rằng phần tử này không có hành động hoặc trạng thái liên kết.

@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 xem phần tử này có truyền tải thông tin mà người dùng sẽ cần để thực hiện nhiệm vụ của họ hay không. Nếu không, bạn nên bỏ phầ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 trên màn hình được lấy làm tiêu điểm một cách độc lập, 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, thì người dùng có thể không hiểu được các phần tử nào 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 sẽ 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 áp dụng cho ListItem; các phần tử trong một mục danh sách được hợp nhất với nhau và các dịch vụ hỗ trợ tiếp cận xem chúng là 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 các dịch vụ hỗ trợ tiếp cận xem chúng là một phần tử duy nhấ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 và một số thông tin bổ sung của người dù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ê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 tham số 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ả thuộc tính ngữ nghĩa của các phần tử con đều đượ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")
        }
    }
}

Các 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 các vùng chứa đó:

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 dùng một trình đọc màn hình như TalkBack để nghe nội dung hiển thị trên màn hình, trước tiên, trình đọc này sẽ chọn toàn bộ mục rồi 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 hay 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 đó. Xin lưu ý rằng bạn cũng phải xoá chính nó 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 biểu tượng đó. Bạn có thể thực hiện việc này 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 phầ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 sử dụng để đọc trạng thái của thành phần kết hợp đó. Ví dụ: thành phần kết hợp bật/tắt được có thể ở trạng thái "đã đánh dấu" hoặc "chưa đánh dấu". Trong một số trường hợp, bạn có thể muốn ghi đè các nhãn mô tả trạng thái mặc định mà Compose sử dụng. Bạn có thể làm như vậy bằng cách chỉ định rõ ràng nhãn mô tả trạng thái trước khi xác định một thành phần kết hợp là 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 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.

Những người dùng cần hỗ trợ tiếp cận gặp khó khăn khi sử dụng một màn hình như vậy. Để hỗ trợ điều hướng, hãy cho biết những phần tử nào là tiêu đề. Trong ví dụ trước, mỗi tiêu đề mục phụ có thể được xác định là một tiêu đề hỗ trợ tiếp cận. Một số dịch vụ hỗ trợ tiếp cận, chẳng hạn như TalkBack, cho phép người dùng di chuyển trực tiếp từ đầu sang hướng khác.

Trong Compose, bạn chỉ định một thành phần kết hợp là một tiêu đề bằng cách xác định thuộc tính semantics của nó:

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

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

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

Giả sử bạn đang thay thế Checkbox của Material bằng cách triển khai của riêng mình. Bạn có thể quên thêm đối tượng sửa đổi triStateToggleable. Đối tượng này 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 cách triển khai thành phần 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.

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

Tài nguyên khác