Accessibility
Accessibility giúp ứng dụng của bạn có thể sử dụng được bởi người khuyết tật. Compose làm cho việc implement accessibility trở nên dễ dàng hơn.
1. Semantics là gì?
Phần tiêu đề “1. Semantics là gì?”Compose sử dụng Semantics tree để cung cấp thông tin cho accessibility services (như TalkBack).
// Compose tự động tạo semantics cho nhiều composablesText("Hello") // Text content tự động được đọc
Button(onClick = { }) { Text("Submit") // Button với label "Submit"}2. Content Descriptions
Phần tiêu đề “2. Content Descriptions”Cho Images
Phần tiêu đề “Cho Images”// ❌ SAI: Không có descriptionImage( painter = painterResource(R.drawable.cat), contentDescription = null // Accessibility bỏ qua image này)
// ✅ ĐÚNG: Có descriptionImage( painter = painterResource(R.drawable.cat), contentDescription = "A cute cat sitting on a couch")Cho Icons
Phần tiêu đề “Cho Icons”// Decorative icon (không cần đọc)Icon(Icons.Default.Star, contentDescription = null)
// Meaningful iconIcon(Icons.Default.Favorite, contentDescription = "Add to favorites")3. semantics modifier
Phần tiêu đề “3. semantics modifier”Thêm hoặc sửa semantics
Phần tiêu đề “Thêm hoặc sửa semantics”@Composablefun CustomAccessibleButton() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable { } .semantics { role = Role.Button contentDescription = "Confirm purchase" } ) { // Custom content }}Merge semantics
Phần tiêu đề “Merge semantics”// Mặc định: mỗi Text được đọc riêngRow { Icon(Icons.Default.Star, null) Text("4.5") Text("(123 reviews)")}// TalkBack đọc: "Star", "4.5", "(123 reviews)"
// Với mergeDescendants: đọc cùng lúcRow( modifier = Modifier.semantics(mergeDescendants = true) { contentDescription = "Rating 4.5 stars from 123 reviews" }) { Icon(Icons.Default.Star, null) Text("4.5") Text("(123 reviews)")}// TalkBack đọc: "Rating 4.5 stars from 123 reviews"4. State Descriptions
Phần tiêu đề “4. State Descriptions”@Composablefun ExpandableCard(expanded: Boolean, onExpand: () -> Unit) { Card( modifier = Modifier .clickable { onExpand() } .semantics { stateDescription = if (expanded) "Expanded" else "Collapsed" } ) { // Content }}Toggle states
Phần tiêu đề “Toggle states”@Composablefun FavoriteButton(isFavorite: Boolean, onToggle: () -> Unit) { IconButton( onClick = onToggle, modifier = Modifier.semantics { contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites" } ) { Icon( if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, contentDescription = null // Description ở IconButton ) }}5. Headings và Structure
Phần tiêu đề “5. Headings và Structure”@Composablefun AccessibleScreen() { Column { Text( "Settings", style = MaterialTheme.typography.headlineLarge, modifier = Modifier.semantics { heading() } )
Text( "Account", style = MaterialTheme.typography.titleMedium, modifier = Modifier.semantics { heading() } )
// Settings items
Text( "Notifications", style = MaterialTheme.typography.titleMedium, modifier = Modifier.semantics { heading() } )
// Notification settings }}6. Custom Actions
Phần tiêu đề “6. Custom Actions”@Composablefun SwipeableItem( item: Item, onDelete: () -> Unit, onArchive: () -> Unit) { Card( modifier = Modifier.semantics { customActions = listOf( CustomAccessibilityAction("Delete") { onDelete() true }, CustomAccessibilityAction("Archive") { onArchive() true } ) } ) { Text(item.title) }}7. Touch Target Size
Phần tiêu đề “7. Touch Target Size”Đảm bảo touch targets đủ lớn (ít nhất 48dp):
@Composablefun SmallIcon() { // ❌ Icon nhỏ, khó click IconButton(onClick = { }) { Icon( Icons.Default.Close, contentDescription = "Close", modifier = Modifier.size(20.dp) ) }
// ✅ IconButton tự động có touch target 48dp // hoặc dùng minimumTouchTargetSize Box( modifier = Modifier .size(24.dp) .minimumInteractiveComponentSize() .clickable { } ) { Icon(Icons.Default.Close, "Close") }}8. Focus và Keyboard Navigation
Phần tiêu đề “8. Focus và Keyboard Navigation”@Composablefun FocusExample() { val focusRequester = remember { FocusRequester() }
Column { TextField( value = "", onValueChange = { }, modifier = Modifier.focusRequester(focusRequester) )
Button(onClick = { focusRequester.requestFocus() }) { Text("Focus TextField") } }}Focus order
Phần tiêu đề “Focus order”@Composablefun CustomFocusOrder() { val (first, second, third) = remember { FocusRequester.createRefs() }
Column { TextField( value = "", onValueChange = { }, modifier = Modifier .focusRequester(second) .focusProperties { next = third previous = first } ) }}9. Live Regions (Announcements)
Phần tiêu đề “9. Live Regions (Announcements)”@Composablefun LiveAnnouncement(message: String?) { message?.let { Text( text = it, modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite // Đợi, không ngắt // hoặc LiveRegionMode.Assertive // Ngắt ngay } ) }}10. Testing Accessibility
Phần tiêu đề “10. Testing Accessibility”Kiểm tra với Compose Test
Phần tiêu đề “Kiểm tra với Compose Test”@Testfun testAccessibility() { composeTestRule.setContent { MyScreen() }
// Kiểm tra content description composeTestRule .onNodeWithContentDescription("Submit button") .assertExists()
// Kiểm tra heading composeTestRule .onNode(hasTestTag("title") and isHeading()) .assertExists()}Accessibility Scanner
Phần tiêu đề “Accessibility Scanner”Sử dụng Accessibility Scanner app để tìm vấn đề accessibility trong ứng dụng.
📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Semantics Property | Mục đích |
|---|---|
contentDescription |
Mô tả cho screen readers |
stateDescription |
Mô tả state (expanded, selected) |
role |
Loại element (Button, Checkbox) |
heading() |
Đánh dấu heading |
customActions |
Actions bổ sung |
liveRegion |
Announce thay đổi |
mergeDescendants |
Gộp children thành 1 unit |
Checklist Accessibility
Phần tiêu đề “Checklist Accessibility”- Tất cả images có contentDescription phù hợp
- Interactive elements có touch target >= 48dp
- Headings được đánh dấu đúng
- Color contrast đủ cao
- State changes được announce
- Test với TalkBack