Giá trị mặc định của API

Theo mặc định, Material, giao diện người dùng Compose và API Foundation triển khai và cung cấp nhiều phương pháp hỗ trợ tiếp cận. Các thành phần này chứa ngữ nghĩa tích hợp theo vai trò và chức năng cụ thể, nghĩa là hầu hết các tính năng hỗ trợ tiếp cận đều được cung cấp mà không cần thêm hoặc ít phải làm gì thêm.

Việc sử dụng API thích hợp cho mục đích thích hợp thường có nghĩa là các thành phần đi kèm với các hành vi hỗ trợ tiếp cận được xác định trước bao gồm các trường hợp sử dụng tiêu chuẩn, nhưng hãy nhớ kiểm tra kỹ xem các chế độ mặc định này có phù hợp với nhu cầu hỗ trợ tiếp cận của bạn hay không. Nếu không, Compose cũng cung cấp các cách để đáp ứng các yêu cầu cụ thể hơn.

Việc nắm được ngữ nghĩa và mẫu hỗ trợ tiếp cận mặc định trong API Compose giúp bạn hiểu cách sử dụng các API đó theo hướng hỗ trợ tiếp cận, cũng như hỗ trợ tiếp cận trong nhiều thành phần tuỳ chỉnh hơn.

Kích thước tối thiểu của đích chạm

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 xác đị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 là 48dp để tuân thủ đúng 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 kích thước tối thiểu này nội bộ, nhưng chỉ khi thành phần có thể nhận được thao tác của người dùng. Ví dụ: khi tham số onCheckedChange của Checkbox được đặt giá trị rỗng, thì hộp đánh dấu sẽ bao gồm khoảng đệm để có chiều rộng và chiều cao ít nhất là 48 dp.

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

Hộp đánh dấu có khoảng đệm mặc định với chiều rộng và chiều cao là 48 dp.
Hình 1. Hộp đánh dấu có khoảng đệm mặc định.

Khi tham số onCheckedChange được đặt thành rỗng, khoảng đệm không được thêm 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ộp đánh dấu không có khoảng đệm.
Hình 2. Hộp đánh dấu không có khoảng đệm.

Khi triển khai các chế độ đ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ẹ bằng cách đặt lệnh gọi lại lượt nhấp trên thành phần kết hợp thành null rồi thêm một đối tượng sửa đổi toggleable hoặc 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)
        }
    }
}

Hộp đánh dấu bên cạnh văn bản "Tuỳ chọn" đang được chọn và bỏ chọn.
Hình 3. Hộp đánh dấu có hành vi nhấp vào được.

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. Compose làm như vậy bằng cách mở rộng kích thước đích chạm nằm ngoài phạm vi của thành phần có thể kết hợp.

Ví dụ sau đây chứa một Box rất nhỏ có thể nhấp. Khu vực đích chạm tự động mở rộng ra ngoài phạm vi của Box, vì vậy, thao tác nhấn vào 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)
        )
    }
}

Một hộp có thể nhấp rất nhỏ được mở rộng thành một mục tiêu chạm lớn hơn bằng cách nhấn vào bên cạnh hộp.
Hình 4. Một hộp có thể nhấp rất nhỏ được mở rộng thành một mục tiêu chạm lớn hơn.

Để ngăn chặn sự trùng lặp có thể xảy ra giữa các khu vực cảm ứng của các 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)
        )
    }
}

Hộp rất nhỏ trong ví dụ trước được tăng kích thước để tạo một mục tiêu chạm lớn hơn.
Hình 5. Mục tiêu chạm hộp lớn hơn.

Phần tử đồ hoạ

Khi bạn xác định 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 nội dung mà ứng dụng đ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ử đồ hoạ.

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 gồm 4 biểu tượng có thể nhấp vào, trong đó biểu tượng "chia sẻ" sáng lên.
Hình 6. Một hàng biểu tượng có thể nhấp vào, trong đó biểu tượng "Chia sẻ" được chọn.

Chỉ dựa vào biểu tượng này, khung Android sẽ không thể mô tả cho người dùng khiếm thị. Khung Android cần có thêm mô tả bằng văn bản của biểu tượng đó.

Tham số contentDescription mô tả một phần tử đồ hoạ. Sử dụng chuỗi đã được bản địa hoá vì người dùng sẽ 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 đồ hoạ chỉ 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 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)
    )
}

contentDescription chủ yếu được dùng cho các phần tử đồ hoạ, chẳng hạn như hình ảnh. Các thành phần Material, chẳng hạn như Button hoặc Text và các hành vi có thể thao tác, chẳng hạn như clickable hoặc toggleable, đi kèm với các ngữ nghĩa được xác định trước khác mô tả hành vi nội tại của chúng và có thể được thay đổi thông qua các API Compose khác.

Yếu tố tương tác

API Material và Foundation Compose tạo các thành phần trên giao diện người dùng mà người dùng có thể tương tác thông qua các API đối tượng sửa đổi clickabletoggleable. Vì các thành phần có thể tương tác có thể bao gồm nhiều phần tử, nên theo mặc định, clickabletoggleable sẽ hợp nhất ngữ nghĩa của các phần tử con để thành phần được coi là một thực thể logic.

Ví dụ: Button Material có thể bao gồm một biểu tượng con và một số văn bản. Thay vì coi các thành phần con là các thành phần riêng lẻ, theo mặc định, Nút Material sẽ hợp nhất ngữ nghĩa của các thành phần con để các dịch vụ hỗ trợ tiếp cận có thể nhóm các thành phần đó theo cách thích hợp:

Các nút có ngữ nghĩa con chưa hợp nhất so với ngữ nghĩa con đã hợp nhất.
Hình 7. Các nút có ngữ nghĩa con chưa hợp nhất so với ngữ nghĩa con đã hợp nhất.

Tương tự, việc sử dụng đối tượng sửa đổi clickable cũng khiến một thành phần kết hợp hợp nhất ngữ nghĩa của các thành phần con thành một thực thể duy nhất. Thực thể này được gửi đến các dịch vụ hỗ trợ tiếp cận bằng cách thể hiện hành động tương ứng:

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

Bạn cũng có thể đặt một onClickLabel cụ thể trên thành phần mẹ có thể nhấp để cung cấp thêm thông tin cho các dịch vụ hỗ trợ tiếp cận và cung cấp nội dung trình bày hành động tinh tế hơn:

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

Lấy TalkBack làm ví dụ, đối tượng sửa đổi clickable này và nhãn lượt nhấp của đối tượng đó sẽ cho phép TalkBack cung cấp gợi ý hành động "Nhấn đúp để mở bài viết này", thay vì phản hồi mặc định chung hơn là "Nhấn đúp để kích hoạt".

Nội dung phản hồi này thay đổi tuỳ thuộc vào loại hành động. Thao tác nhấn và giữ sẽ cung cấp gợi ý TalkBack là "Nhấn đúp và giữ để", theo sau là một nhãn:

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

Trong một số trường hợp, bạn có thể không có quyền truy cập trực tiếp vào đối tượng sửa đổi clickable (ví dụ: khi đối tượng này được đặt ở một vị trí nào đó trong lớp lồng nhau thấp hơn), nhưng vẫn muốn thay đổi nhãn thông báo từ mặc định. Để thực hiện việc này, hãy tách việc đặt clickable khỏi việc sửa đổi thông báo bằng cách sử dụng đối tượng sửa đổi semantics và đặt nhãn lượt nhấp ở đó để sửa đổi nội dung thể hiện hành động:

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

Trong trường hợp này, bạn không cần truyền thao tác nhấp hai lần, vì các API Compose hiện có, chẳng hạn như clickable hoặc Button, sẽ xử lý thao tác này cho bạn. Điều này là do logic hợp nhất đảm bảo rằng nhãn và thao tác sửa đổi ngoài cùng được lấy cho thông tin hiện có.

Trong ví dụ trước, thao tác nhấp openArticle() được NestedArticleListItem tự động truyền sâu xuống ngữ nghĩa clickable và có thể được để trống trong thao tác đối tượng sửa đổi ngữ nghĩa thứ hai. Tuy nhiên, nhãn lượt nhấp được lấy từ đối tượng sửa đổi ngữ nghĩa thứ hai onClick(label = "Open this article"), vì nhãn này không có trong đối tượng sửa đổi ngữ nghĩa đầu tiên.

Bạn có thể gặp phải các trường hợp mà bạn muốn ngữ nghĩa của phần tử con được hợp nhất vào phần tử mẹ, nhưng điều đó không xảy ra. Hãy xem phần Hợp nhất và xoá để biết thêm thông tin chi tiết.

Thành phần tuỳ chỉnh

Đối với các thành phần tuỳ chỉnh, theo quy tắc chung, hãy xem cách triển khai một thành phần tương tự trong thư viện Material hoặc các thư viện Compose khác, đồng thời bắt chước hoặc sửa đổi hành vi hỗ trợ tiếp cận của thành phần đó khi cần thiết.

Ví dụ: nếu bạn đang thay thế Checkbox trong Material bằng phương thức triển khai của riêng mình, thì việc xem phương thức triển khai Hộp đánh dấu hiện có sẽ nhắc bạn thêm đối tượng sửa đổi triStateToggleable. Đối tượng này sẽ xử lý các thuộc tính hỗ trợ tiếp cận cho thành phần này.

Ngoài ra, hãy tận dụng nhiều thành phần sửa đổi Foundation, 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, cũng như các phương pháp Compose hiện có được đề cập trong phần này.

Bạn cũng có thể tìm thấy ví dụ về thành phần nút bật/tắt tuỳ chỉnh trong phần Xoá và đặt ngữ nghĩa, cũng như thông tin chi tiết hơn về cách hỗ trợ chức năng hỗ trợ tiếp cận trong các thành phần tuỳ chỉnh trong nguyên tắc về API.