Hỗ trợ tiếp cận trong Compose

Các ứng dụng được viết trong Compose phải hỗ trợ khả năng tiếp cận theo nhu cầu đa dạng của người dùng. Dịch vụ hỗ trợ tiếp cận được dùng để chuyển đổi nội dung hiển thị trên màn hình sang định dạng phù hợp hơn cho người dùng có nhu cầu đặc biệt. Để hỗ trợ các dịch vụ hỗ trợ tiếp cận, các ứng dụng sẽ sử dụng API trong khung Android để cấp quyền truy cập thông tin ngữ nghĩa về các phần tử giao diện người dùng. Sau đó, khung Android sẽ thông báo cho các dịch vụ hỗ trợ tiếp cận về thông tin ngữ nghĩa này. Mỗi dịch vụ hỗ trợ tiếp cận có thể chọn cách hiệu quả nhất để mô tả ứng dụng cho người dùng. Android cung cấp một số dịch vụ hỗ trợ tiếp cận, trong đó có TalkbackSwitch Access (Tiếp cận bằng công tắc).

Ngữ nghĩa

Compose sử dụng thuộc tính ngữ nghĩa để truyền thông tin đến các dịch vụ hỗ trợ tiếp cận. Thuộc tính ngữ nghĩa cung cấp thông tin về các thành phần trên giao diện người dùng được hiển thị cho người dùng. Hầu hết thành phần có thể kết hợp được tích hợp sẵn như TextButton điền vào các thuộc tính ngữ nghĩa này bằng thông tin suy ra từ các thành phần có thể kết hợp và các con của nó. Một số thành phần sửa đổi như toggleableclickable cũng sẽ đặt một số thuộc tính ngữ nghĩa. Tuy nhiên, đôi khi khung này cần thêm thông tin để hiểu cách mô tả một thành phần trên giao diện người dùng cho người dùng.

Tài liệu này mô tả các tình huống mà bạn cần thêm thông tin bổ sung rõ ràng vào một thành phần có thể kết hợp để có thể mô tả nó một cách chính xác vào khung Android. Tài liệu này cũng giải thích cách thay thế hoàn toàn thông tin về ngữ nghĩa cho một thành phần có thể kết hợp. Tài liệu này giả định người đọc có sự hiểu biết cơ bản về hỗ trợ tiếp cận trong Android.

Các trường hợp sử dụng phổ biến

Để 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, ứng dụng của bạn phải tuân thủ các phương pháp hay nhất mô tả trên trang này.

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 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 Hướng dẫn hỗ trợ tiếp cận 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ì lớp đó sẽ bao gồm khoảng đệm để có chiều rộng và chiều cao ít nhất là 48dp.

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

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
fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Khi triển khai các đ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 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
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. 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.

Trong ví dụ sau, chúng tôi tạo 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, việc nhấn vào bên cạnh Box sẽ vẫn kích hoạt sự kiện nhấp.

@Composable
fun DefaultPreview() {
   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 các thành phần kết hợp, bạn phải 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ụ của chúng tôi đưa ra, điều đó có nghĩa là sử dụng thành phần sửa đổi sizeIn để đặt kích thước tối thiểu cho hộp bên trong:

@Composable
fun DefaultPreview() {
   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ả điều 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 nhấp chuột giúp mô tả ứng dụng cho người dùng có nhu cầu cụ thể.

Đặt nhãn nhấp chuột bằng cách truyền một tham số trong công cụ sửa đổi clickable:

@Composable
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 không có quyền truy cập vào công cụ sửa đổi có thể nhấp được, bạn có thể đặt nhãn nhấp chuột trong công cụ sửa đổi ngữ nghĩa:

@Composable
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 có thể kết hợp Image hoặc Icon, không có cách tự động nào để khung Android hiểu nội dung đang được hiển thị. 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, trong đó biểu tượng "chia sẻ" sáng lên

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

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

Một số phần tử hình ảnh 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
fun PostImage(post: Post, modifier: Modifier = Modifier) {
  val image = post.imageThumb ?: imageResource(R.drawable.placeholder_1_1)

  Image(
    bitmap = 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ử đó có chuyển tải thông tin mà người dùng cần để thực hiện công việc của họ hay không. Nếu không, bạn nên chừa phần nội dung mô tả ra.

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. Nếu mỗi một thành phần có thể kết hợp cấp thấp trên màn hình đều được lấy làm tiêu điểm một cách độc lập, thì người dùng sẽ phải tương tác nhiều để di chuyển trên màn hình. Nếu các phần tử được hợp nhất quá thường xuyên, thì người dùng có thể không hiểu những phần tử nào thuộc về nhau.

Khi bạn áp dụng một công cụ sửa đổi clickable cho thành phần kết hợp, thì Compose sẽ tự động hợp nhất tất cả các phần tử có trong đó. Điều này cũng áp dụng cho ListItem; các phần tử trong một mục danh sách sẽ được hợp nhất và các dịch vụ hỗ trợ tiếp cận sẽ xem các phần tử đó là một phần tử duy nhấ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 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 có thể 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ể yêu cầu Compose hợp nhất các phần tử này bằng cách sử dụng tham số mergeDescendants trong thành phần sửa đổi semantics. Bằng cách này, các dịch vụ hỗ trợ tiếp cận sẽ chỉ chọn yếu 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 đề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")
    }
  }
}

Giờ đây, các dịch vụ Hỗ trợ tiếp cận sẽ 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 sử dụng một trình đọc màn hình như Talkback để lắng nghe nội dung hiển thị trên màn hình thì trước tiên, trình đọc 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. Cách tốt hơn là nên xác định một thao tác tuỳ chỉnh cho phép người dùng đánh dấu mục. Hãy lưu ý rằng bạn cũng sẽ phải xoá hoàn toàn hành vi của biểu tượng dấu trang để đảm bảo rằng dịch vụ hỗ trợ tiếp cận sẽ không chọn biểu tượng đó. Bạn có thể thực hiện việc này bằng thành phần sửa đổi clearAndSetSemantics:

@Composable
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ử

thành phần có thể 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 có thể kết hợp đó. Ví dụ: một thành phần có thể kết hợp có thể chuyển đổi có thể ở trạng thái "Đã đánh dấu" hoặc "Bỏ đánh dấu". Trong một số trường hợp, bạn nê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 việc nà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 có thể kết hợp có thể chuyển đổi:

@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 điều hướng trên một màn hình. Để hỗ trợ điều hướng, bạn có thể chỉ báo các phần tử nào là tiêu đề. Trong ví dụ trên, bạn có thể xác định mỗi tiêu đề phụ là tiêu đề hỗ trợ tiếp cận. Một số 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ừ tiêu đề này sang tiêu đề khác.

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

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

Tạo thành phần kết hợp tuỳ chỉnh cấp thấp

Một trường hợp sử dụng nâng cao hơn bao gồm việc thay thế một số thành phần Material trong ứng dụng với các phiên bản tuỳ chỉnh. Trong tình huống này, điều quan trọng là bạn phải chú ý đến việc hỗ trợ tiếp cận. Giả sử bạn đang thay thế Checkbox trong Material bằng phương pháp triển khai của riêng mình. Bạn sẽ rất dễ quên việc thêm thành phần sửa đổi triStateToggleablechịu trách nhiệm 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 may rủi, bạn nên xem xét 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 nhớ 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ìm hiểu thêm

Để tìm hiểu thêm về hỗ trợ tiếp cận trong mã Compose, tham khảo lớp học lập trình Hỗ trợ tiếp cận trong Jetpack Compose.