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

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng Jetpack Compose để cải thiện khả năng hỗ trợ tiếp cận của ứng dụng. Chúng ta sẽ thảo luận một số trường hợp sử dụng phổ biến và cải thiện ứng dụng mẫu theo từng bước. Chúng ta sẽ đề cập đến các chủ đề như kích thước đích nhấn, mô tả nội dung, nhãn lượt nhấp, v.v.

Người mắc suy giảm thị lực, mù màu, suy giảm thính lực, suy giảm độ khéo léo, khuyết tật trí tuệ và nhiều dạng khuyết tật khác sử dụng thiết bị Android để hoàn thành các công việc trong cuộc sống thường ngày của họ. Bằng việc phát triển ứng dụng có khả năng hỗ trợ tiếp cận, bạn có thể cải thiện trải nghiệm người dùng, đặc biệt là dành cho người dùng có các nhu cầu nói trên cũng như các nhu cầu hỗ trợ tiếp cận khác.

Trong lớp học lập trình này, chúng ta sử dụng TalkBack để kiểm thử các thay đổi về mã theo cách thủ công. TalkBack là một dịch vụ hỗ trợ tiếp cận chủ yếu được người khiếm thị sử dụng. Đừng quên kiểm thử mọi thay đổi mã với các dịch vụ hỗ trợ tiếp cận khác, chẳng hạn như Switch Access (Tiếp cận bằng công tắc).

Hình chữ nhật tâm điểm TalkBack di chuyển qua màn hình chính của Jetnews. Văn bản mà TalkBack thông báo xuất hiện ở cuối màn hình.

TalkBack đang hoạt động trong ứng dụng Jetnews.

Kiến thức bạn sẽ học được

Trong lớp học lập trình này, bạn sẽ tìm hiểu:

  • Cách phục vụ người dùng bị suy giảm độ khéo léo bằng cách tăng kích thước đích nhấn.
  • Thuộc tính ngữ nghĩa là gì và cách để thay đổi loại thuộc tính này.
  • Cách cung cấp thông tin để các thành phần kết hợp (composable) trở nên dễ tiếp cận hơn.

Bạn cần có

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, chúng ta sẽ cải thiện khả năng hỗ trợ tiếp cận của một ứng dụng đọc tin tức. Chúng ta sẽ bắt đầu bằng một ứng dụng chưa có các tính năng hỗ trợ tiếp cận thiết yếu, đồng thời áp dụng những kiến thức mà chúng ta học được để làm cho ứng dụng trở nên hữu ích hơn cho người có nhu cầu hỗ trợ tiếp cận.

2. Thiết lập

Trong bước này, bạn sẽ tải mã xuống cho lớp học lập trình này. Mã này bao gồm một ứng dụng đơn giản để đọc tin tức.

Bạn cần có

Lấy mã

Bạn có thể tìm thấy mã cho lớp học lập trình này trong kho lưu trữ codelab-android-compose trên GitHub. Để sao chép, hãy chạy:

$ git clone https://github.com/android/codelab-android-compose

Ngoài ra, bạn có thể tải 2 tệp zip xuống:

Xem xét ứng dụng mẫu

Mã bạn vừa tải xuống có chứa mã dành cho tất cả lớp học lập trình Compose hiện có. Để hoàn tất lớp học lập trình này, hãy mở dự án AccessibilityCodelab trong Android Studio.

Bạn nên bắt đầu bằng mã trong nhánh main và làm theo hướng dẫn từng bước của lớp học lập trình theo tiến độ phù hợp với bạn.

Thiết lập TalkBack

Trong lớp học lập trình này, chúng ta sẽ sử dụng TalkBack để kiểm tra các thay đổi. Khi bạn sử dụng thiết bị thực để kiểm thử, hãy làm theo hướng dẫn này để bật TalkBack. Theo mặc định, TalkBack không có sẵn trong các trình mô phỏng. Hãy chọn một trình mô phỏng chứa Cửa hàng Play rồi tải Android Accessibility Suite (Bộ hỗ trợ tiếp cận của Android) xuống.

3. Kích thước đích chạm

Bất kỳ thành phần nào trên màn hình mà một người có thể nhấp, chạm vào hoặc tương tác đều phải đủ lớn để đảm bảo độ tin cậy cho tương tác đó. Bạn nên đảm bảo các phần tử này có chiều rộng và chiều cao ít nhất là 48dp.

Nếu các đối tượng điều khiển này có kích thước động hoặc thay đổi kích thước dựa trên kích thước nội dung, thì bạn nên cân nhắc sử dụng đối tượng sửa đổi (modifier) sizeIn để đặt ngưỡng dưới cho các kích thước.

Một số thành phần Material thay đổi những kích thước này cho bạn. Ví dụ: thành phần kết hợp Button (Nút) có MinHeight được đặt thành 36dp và sử dụng khoảng đệm dọc 8dp. Giá trị này phải được tăng lên để đạt chiều cao cần thiết là 48dp.

Khi mở ứng dụng mẫu và chạy TalkBack, chúng ta nhận thấy biểu tượng chữ thập trong các thẻ tin bài có đích nhấn rất nhỏ. Chúng ta muốn đích nhấn này ít nhất phải là 48dp.

Đây là ảnh chụp màn hình ứng dụng gốc ở bên trái so với bản đã cải thiện ở bên phải.

So sánh trên cùng một mục danh sách, đường viền nhỏ của biểu tượng chữ thập ở bên trái, còn đường viền lớn ở bên phải.

Hãy xem quá trình triển khai và kiểm tra kích thước của thành phần kết hợp này. Mở PostCards.kt rồi tìm thành phần kết hợp PostCardHistory. Như bạn có thể thấy, quá trình triển khai sẽ đặt kích thước của biểu tượng trình đơn mục bổ sung thành 24dp:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...

   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .size(24.dp)
           )
       }
   }
   // ...
}

Để tăng kích thước đích nhấn cho Icon này, chúng ta có thể thêm khoảng đệm:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .padding(12.dp)
                   .size(24.dp)
           )
       }
   }
   // ...
}

Trong trường hợp sử dụng của chúng ta, có một cách dễ dàng hơn để đảm bảo đích nhấn có kích thước ít nhất là 48 dp. Chúng ta có thể sử dụng thành phần Material IconButton để xử lý vấn đề này:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

Giờ đây, khi bạn dùng màn hình TalkBack, TalkBack sẽ hiện chính xác vùng đích nhấn là 48 dp. Ngoài ra, IconButton cũng thêm chỉ báo gợn sóng (ripple), cho người dùng biết rằng phần tử này nhấp vào được.

4. Nhãn lượt nhấp

Theo mặc định, thành phần nhấp vào được trong ứng dụng không đưa ra thông tin nào về điều sẽ xảy ra khi nhấp vào thành phần đó. Do đó, các dịch vụ hỗ trợ tiếp cận như TalkBack sẽ sử dụng một nội dung mô tả mặc định rất chung.

Để mang lại trải nghiệm tốt nhất cho người dùng có nhu cầu hỗ trợ tiếp cận, chúng ta có thể cung cấp nội dung mô tả cụ thể để giải thích điều sẽ xảy ra khi người dùng nhấp vào thành phần này.

Trong ứng dụng Jetnews, người dùng có thể nhấp vào các thẻ tin bài để đọc toàn bộ tin bài. Theo mặc định, thao tác này sẽ đọc to nội dung của phần tử nhấp vào được, tiếp theo là văn bản "Double tap to activate" ("Nhấn đúp để kích hoạt"). Thay vào đó, chúng ta muốn cụ thể hơn và sử dụng "Double tap to read article" ("Nhấn đúp để đọc bài viết"). Đây là giao diện của phiên bản gốc, so với giải pháp lý tưởng của chúng ta:

Hai bản ghi màn hình đang sử dụng TalkBack, nhấn vào một tin bài trong danh sách dọc và một tin bài trong băng chuyền ngang.

Thay đổi nhãn lượt nhấp (click label) của một thành phần kết hợp. Trước (ở bên trái) so với sau (ở bên phải).

Đối tượng sửa đổi clickable bao gồm một tham số cho phép bạn trực tiếp thiết lập nhãn của lượt nhấp này.

Hãy cùng xem lại việc triển khai PostCardHistory:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

Như bạn có thể thấy, quá trình triển khai này sử dụng đối tượng sửa đổi clickable. Để đặt nhãn lượt nhấp, chúng ta có thể đặt giá trị cho tham số onClickLabel:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable(
               // R.string.action_read_article = "read article"
               onClickLabel = stringResource(R.string.action_read_article)
           ) {
               navigateToArticle(post.id)
           }
   ) {
       // ...
   }
}

TalkBack hiện thông báo chính xác "Double tap to read article" ("Nhấn đúp để đọc bài viết").

Các thẻ tin bài khác trên màn hình chính có cùng nhãn lượt nhấp chung (generic click label). Hãy xem việc triển khai và cập nhật nhãn lượt nhấp của thành phần kết hợp PostCardPopular:

@Composable
fun PostCardPopular(
   // ...
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

Thành phần này có thể sử dụng nội bộ Card, điều này không cho phép bạn đặt trực tiếp nhãn nhấp chuột. Thay vào đó, bạn có thể sử dụng đối tượng sửa đổi semantics để đặt nhãn lượt nhấp:

@Composable
fun PostCardPopular(
   post: Post,
   navigateToArticle: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   val readArticleLabel = stringResource(id = R.string.action_read_article)
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
          .size(280.dp, 240.dp)
          .semantics { onClick(label = readArticleLabel, action = null) },
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

5. Thao tác tuỳ chỉnh

Nhiều ứng dụng cho thấy một số loại danh sách, trong đó mỗi mục trong danh sách lại chứa một hoặc nhiều thao tác. Khi sử dụng trình đọc màn hình, việc điều hướng một danh sách như vậy có thể trở nên tẻ nhạt, vì cùng một thao tác sẽ được lấy tâm điểm lặp đi lặp lại.

Thay vào đó, chúng ta có thể thêm các thao tác hỗ trợ tiếp cận tuỳ chỉnh vào một thành phần kết hợp. Bằng cách này, bạn có thể nhóm các thao tác liên quan đến cùng một mục danh sách lại với nhau.

Trong ứng dụng Jetnews, chúng ta đang hiện danh sách bài viết mà người dùng có thể đọc. Mỗi mục danh sách chứa một thao tác để cho biết người dùng muốn xem bớt nội dung về chủ đề này. Trong phần này, chúng ta sẽ di chuyển thao tác này sang một thao tác hỗ trợ tiếp cận tuỳ chỉnh, nhờ vậy, việc điều hướng trong danh sách sẽ dễ dàng hơn.

Ở bên trái, bạn có thể thấy tình huống mặc định, trong đó mỗi biểu tượng chữ thập đều có thể làm tâm điểm. Ở bên phải, bạn có thể thấy giải pháp, trong đó thao tác này được đưa vào các thao tác tuỳ chỉnh trong TalkBack:

Hai bản ghi màn hình đang bật TalkBack. Màn hình bên trái cho biết cách chọn biểu tượng chữ thập trên mục tin bài. Khi nhấn đúp, một hộp thoại sẽ mở ra. Màn hình ở bên phải cho thấy việc dùng cử chỉ nhấn ba lần để mở trình đơn tuỳ chỉnh Actions (Thao tác). Thao tác nhấn vào "Ẩn bớt nội dung này" sẽ mở ra hộp thoại tương tự.

Thêm thao tác tuỳ chỉnh vào một mục tin bài. Trước (ở bên trái) so với sau (ở bên phải).

Hãy mở PostCards.kt và xem việc triển khai thành phần kết hợp PostCardHistory. Hãy lưu ý các thuộc tính nhấp vào được của cả RowIconButton bằng cách sử dụng Modifier.clickableonClick:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

Theo mặc định, cả thành phần kết hợp RowIconButton đều có thể nhấp vào được. Vì vậy, TalkBack sẽ lấy tâm điểm vào đó. Việc này xảy ra cho từng mục trong danh sách, nghĩa là rất nhiều thao tác vuốt khi điều hướng trong danh sách. Chúng ta muốn đưa thao tác liên quan đến IconButton vào thao tác tuỳ chỉnh trên mục danh sách. Chúng ta có thể yêu cầu Dịch vụ hỗ trợ tiếp cận không tương tác với Icon này bằng cách sử dụng đối tượng sửa đổi clearAndSetSemantics:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

Tuy nhiên, nếu xoá ngữ nghĩa của IconButton thì không còn cách để thực thi thao tác này nữa. Chúng ta có thể thêm thao tác này vào mục trong danh sách bằng cách thêm một thao tác tuỳ chỉnh trong đối tượng sửa đổi semantics:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   val showFewerLabel = stringResource(R.string.cd_show_fewer)
   Row(
        Modifier
            .clickable(
                onClickLabel = stringResource(R.string.action_read_article)
            ) {
                navigateToArticle(post.id)
            }
            .semantics {
                customActions = listOf(
                    CustomAccessibilityAction(
                        label = showFewerLabel,
                        // action returns boolean to indicate success
                        action = { openDialog = true; true }
                    )
                )
            }
   ) {
       // ...
       CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = showFewerLabel
                )
            }
       }
   }
   // ...
}

Bây giờ, chúng ta có thể dùng cửa sổ thao tác tuỳ chỉnh bật lên trong TalkBack để áp dụng thao tác đó. Việc này ngày càng cần đến hơn khi số lượng thao tác bên trong một mục danh sách tăng lên.

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

Không phải người dùng ứng dụng nào cũng có thể xem hoặc diễn giải thành phần hình ảnh xuất hiện trong ứng dụng, chẳng hạn như biểu tượng và hình minh hoạ. Ngoài ra, không có cách nào để các dịch vụ hỗ trợ tiếp cận hiểu được các yếu tố hình ảnh nếu chỉ dựa trên pixel của hình ảnh. Là nhà phát triển, bạn cần cung cấp thêm thông tin về các thành phần hình ảnh trong ứng dụng cho các dịch vụ hỗ trợ tiếp cận.

Các thành phần kết hợp dạng hình ảnh như ImageIcon chứa một tham số contentDescription. Trong tham số này, bạn truyền thông tin mô tả đã bản địa hoá cho thành phần hình ảnh đó hoặc null nếu thành phần hình ảnh chỉ mang tính chất trang trí.

Trong ứng dụng của chúng ta, màn hình bài viết thiếu một số thông tin mô tả nội dung. Hãy chạy ứng dụng rồi chọn bài viết trên cùng để điều hướng đến màn hình bài viết.

Hai bản ghi màn hình đang bật TalkBack, nhấn vào nút quay lại trong màn hình bài viết. Màn hình bên trái gọi ra thông báo "Nút — nhấn đúp để kích hoạt". Màn hình bên phải gọi ra thông báo "Di chuyển lên — nhấn đúp để kích hoạt".

Thêm thông tin mô tả cho nội dung hình ảnh. Trước (ở bên trái) so với sau (ở bên phải).

Khi chúng ta không đưa ra thông tin nào, biểu tượng điều hướng trên cùng bên trái sẽ chỉ thông báo "Button, double tap to activate" ("Nút — nhấn đúp để kích hoạt"). Thông báo này không cho người dùng biết thông tin gì về thao tác sẽ được thực hiện khi họ kích hoạt nút đó. Hãy mở ArticleScreen.kt:

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = null
                       )
                   }
               }
           )
       }
   ) { 
       // ...
   }
}

Thêm thông tin mô tả nội dung có ý nghĩa cho Icon (Biểu tượng):

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = stringResource(
                               R.string.cd_navigate_up
                           )
                       )
                   }
               }
           )
       }
   ) { 
       // ...
   }
}

Một phần tử hình ảnh khác trong bài viết này là hình ảnh tiêu đề. Trong trường hợp này, hình ảnh này chỉ mang tính chất trang trí và không biểu thị nội dung nào mà chúng ta cần truyền tải cho người dùng. Do đó, thông tin mô tả nội dung được đặt thành null và phần tử này bị bỏ qua khi chúng ta sử dụng dịch vụ hỗ trợ tiếp cận.

Phần tử hình ảnh cuối cùng trong màn hình của chúng ta là ảnh hồ sơ. Trong trường hợp này, chúng ta sử dụng một hình đại diện chung, nên bạn không cần thêm thông tin mô tả nội dung ở đây. Khi sử dụng ảnh hồ sơ thực tế của tác giả này, chúng ta có thể yêu cầu tác giả cung cấp thông tin mô tả nội dung phù hợp cho ảnh đó.

7. Tiêu đề

Khi một màn hình chứa nhiều văn bản (chẳng hạn như màn hình bài viết), người dùng gặp khó khăn về thị giác rất khó tìm nhanh được phần mà họ cần. Để trợ giúp họ, chúng ta có thể cho biết phần văn bản nào là tiêu đề. Sau đó, người dùng có thể nhanh chóng điều hướng trên các tiêu đề bằng cách vuốt lên hoặc xuống.

Theo mặc định, không thành phần kết hợp nào được đánh dấu là tiêu đề, vì vậy sẽ không thể điều hướng được. Chúng ta muốn màn hình bài viết của chúng ta cung cấp tính năng điều hướng theo từng tiêu đề một:

Hai bản ghi màn hình đang bật TalkBack, sử dụng chức năng vuốt xuống để điều hướng qua các tiêu đề. Màn hình bên trái đọc to "Không có tiêu đề tiếp theo". Màn hình bên phải điều hướng qua các tiêu đề và đọc to từng tiêu đề.

Thêm tiêu đề. Trước (ở bên trái) so với sau (ở bên phải).

Các tiêu đề trong bài viết của chúng ta được khai báo trong PostContent.kt. Hãy mở tệp đó rồi cuộn đến thành phần kết hợp Paragraph:

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp),
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

Ở đây, Header được khai báo là một thành phần kết hợp Text đơn giản. Chúng ta có thể đặt thuộc tính ngữ nghĩa heading để cho biết thành phần kết hợp này là một tiêu đề.

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp)
                     .semantics { heading() },
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

8. Hợp nhất tuỳ chỉnh

Như chúng ta đã thấy trong các bước trước, các dịch vụ hỗ trợ tiếp cận như TalkBack điều hướng trên màn hình theo từng phần tử một. Theo mặc định, mỗi thành phần kết hợp cấp độ thấp trong Jetpack Compose lại đặt ít nhất một thuộc tính ngữ nghĩa sẽ nhận được tâm điểm. Ví dụ: một thành phần kết hợp Text đặt thuộc tính ngữ nghĩa text và do đó nhận được tâm điểm.

Tuy nhiên, việc có quá nhiều phần tử có thể làm tâm điểm lại có thể gây nhầm lẫn khi người dùng điều hướng theo từng phần tử một. Thay vào đó, bạn có thể hợp nhất các thành phần kết hợp với nhau bằng đối tượng sửa đổi semantics chứa thuộc tính mergeDescendants tương ứng.

Hãy kiểm tra màn hình bài viết. Hầu hết phần tử đều có được cấp độ tâm điểm phù hợp. Tuy nhiên, siêu dữ liệu của bài viết hiện được đọc to thành nhiều mục riêng biệt. Bạn có thể cải thiện việc này bằng cách hợp nhất các phần tử đó thành một thực thể có thể làm tâm điểm:

Hai bản ghi màn hình đang bật TalkBack. Màn hình bên trái cho thấy các hình chữ nhật TalkBack màu xanh lục riêng biệt dành cho các trường Author (Tác giả) và Metadata (Siêu dữ liệu). Màn hình bên phải cho thấy một hình chữ nhật xung quanh cả hai trường và đọc nội dung được nối lại.

Hợp nhất các thành phần kết hợp. Trước (ở bên trái) so với sau (ở bên phải).

Hãy mở PostContent.kt và kiểm tra thành phần kết hợp PostMetadata:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
               Text(
                   // ..
               )
           }
       }
   }
}

Chúng ta có thể yêu cầu hàng cấp cao nhất hợp nhất các thành phần con. Việc này sẽ dẫn đến hành vi mà chúng ta muốn:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row(Modifier.semantics(mergeDescendants = true) {}) {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
               Text(
                   // ..
               )
           }
       }
   }
}

9. Nút chuyển và hộp đánh dấu

Các thành phần có thể bật/tắt như SwitchCheckbox sẽ đọc to trạng thái đã đánh dấu khi được TalkBack chọn. Nếu không có ngữ cảnh thì có thể sẽ khó mà hiểu được ý nghĩa của những thành phần có thể bật/tắt này. Chúng ta có thể thêm ngữ cảnh cho thành phần có thể bật/tắt bằng cách nâng trạng thái có thể bật/tắt lên để người dùng có thể bật/tắt Switch hoặc Checkbox bằng cách nhấn vào chính thành phần kết hợp đó hoặc nhấn vào nhãn mô tả thành phần kết hợp đó.

Chúng ta có thể xem ví dụ về việc này trong màn hình Interests (Mối quan tâm). Bạn có thể điều hướng đến đó bằng cách mở ngăn điều hướng trên Màn hình chính. Trên màn hình Interests (Mối quan tâm), chúng ta có danh sách chủ đề mà người dùng có thể đăng ký. Theo mặc định, các hộp đánh dấu trên màn hình này được lấy tiêu điểm mà không có liên hệ với nhãn. Điều này khiến người dùng khó mà hiểu được ngữ cảnh của hộp đánh dấu. Chúng ta muốn toàn bộ Row đều bật/tắt được:

Hai bản ghi màn hình đang bật TalkBack, cho thấy màn hình mối quan tâm với danh sách chủ đề chọn được. Trên màn hình bên trái, TalkBack chọn riêng từng hộp đánh dấu. Trên màn hình bên phải, TalkBack chọn toàn bộ hàng.

Xử lý hộp đánh dấu. Trước (ở bên trái) so với sau (ở bên phải).

Hãy mở InterestsScreen.kt và xem cách triển khai thành phần kết hợp TopicItem:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = { onToggle() },
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

Như bạn có thể thấy ở đây, Checkbox có lệnh gọi lại onCheckedChange xử lý việc bật/tắt phần tử. Chúng ta có thể nâng lệnh gọi lại này lên toàn bộ Row:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

10. Mô tả trạng thái

Trong bước trước, chúng ta đã nâng hành vi bật/tắt từ lớp con Checkbox lên lớp mẹ Row. Chúng ta có thể cải thiện khả năng hỗ trợ tiếp cận của thành phần này hơn nữa bằng cách thêm thông tin mô tả tuỳ chỉnh cho trạng thái của thành phần kết hợp.

Theo mặc định, trạng thái Checkbox sẽ được đọc là "Ticked" ("Đã đánh dấu") hoặc "Not ticked" ("Chưa đánh dấu"). Chúng ta có thể thay thế thông tin mô tả này bằng thông tin mô tả tuỳ chỉnh của riêng mình:

Hai bản ghi màn hình đang bật TalkBack, nhấn vào một chủ đề trong màn hình mối quan tâm. Màn hình bên trái thông báo "Chưa đánh dấu", còn màn hình bên phải thông báo "Chưa đăng ký".

Thêm nội dung mô tả trạng thái Trước (ở bên trái) so với sau (ở bên phải).

Chúng ta có thể tiếp tục với thành phần kết hợp TopicItem mà chúng ta đã điều chỉnh ở bước cuối cùng:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

Chúng ta có thể thêm nội dung mô tả trạng thái tuỳ chỉnh bằng cách sử dụng thuộc tính stateDescription bên trong đối tượng sửa đổi semantics:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
   val stateSubscribed = stringResource(R.string.state_subscribed)
   Row(
       modifier = Modifier
           .semantics {
               stateDescription = if (selected) {
                   stateSubscribed
               } else {
                   stateNotSubscribed
               }
           }
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

11. Xin chúc mừng!

Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu thêm về tính năng hỗ trợ tiếp cận trong Compose. Bạn đã tìm hiểu về đích nhấn, thông tin mô tả phần tử hình ảnh và thông tin mô tả trạng thái. Bạn đã thêm nhãn lượt nhấp, tiêu đề và thao tác tuỳ chỉnh. Bạn đã nắm được cách thêm yếu tố hợp nhất tuỳ chỉnh cũng như cách xử lý nút chuyển và hộp đánh dấu. Việc áp dụng những kiến thức này vào ứng dụng sẽ giúp ứng dụng cải thiện đáng kể khả năng hỗ trợ tiếp cận!

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose. Cũng như các mã mẫu khác, bao gồm cả Jetnews.

Tài liệu

Để biết thêm thông tin và hướng dẫn về những chủ đề này, hãy xem các tài liệu sau: