Hợp nhất và xoá

Khi các dịch vụ hỗ trợ tiếp cận điều hướng qua các phần tử trên màn hình, điều quan trọng là các phần tử này phải được nhóm, tách riêng hoặc thậm chí là ẩn ở mức độ 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 đều được làm nổi bật 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ử đượ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. Nếu có các phần tử trên màn hình chỉ mang tính trang trí, thì bạn có thể ẩn các phần tử này khỏi dịch vụ hỗ trợ tiếp cận. Trong những trường hợp này, bạn có thể sử dụng API Compose để hợp nhất, xoá và ẩn ngữ nghĩa.

Hợp nhất ngữ nghĩa

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 mẹ, Compose sẽ tự động hợp nhất tất cả các phần tử con trong đó. Để hiểu cách các thành phần Material và Foundation tương tác của Compose sử dụng chiến lược hợp nhất theo mặc định, hãy xem phần Thành phần tương tác.

Một thành phần thường bao gồm nhiều thành phần kết hợp. Các thành phần kết hợp này có thể tạo thành một nhóm logic và mỗi thành phần có thể chứa thông tin quan trọng, nhưng bạn vẫn có thể 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 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.
Hình 1. 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 ngữ nghĩa. Bằng cách này, các dịch vụ hỗ trợ tiếp cận sẽ coi thành phần này là một thực thể 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 và hợp nhất nội dung của 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.
Hình 2. 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.

Mỗi thuộc tính ngữ nghĩa có một chiến lược hợp nhất xác định. Ví dụ: thuộc tính ContentDescription thêm tất cả các giá trị con trong ContentDescription vào một danh sách. Bạn có thể kiểm tra chiến lược hợp nhất của một thuộc tính ngữ nghĩa bằng cách kiểm tra cách triển khai mergePolicy của thuộc tính đó trong SemanticsProperties.kt. Các thuộc tính có thể lấy giá trị gốc hoặc giá trị con, hợp nhất các giá trị thành danh sách hoặc chuỗi, hoàn toàn không cho phép hợp nhất và xoá ngoại lệ hoặc bất kỳ chiến lược hợp nhất tuỳ chỉnh nào khác.

Có những trường hợp khác mà bạn muốn hợp nhất ngữ nghĩa con thành ngữ nghĩa mẹ, nhưng điều đó không xảy ra. Trong ví dụ sau, chúng ta có phần tử mẹ của mục danh sách clickable cùng với các phần tử con và chúng ta có thể mong đợi phần tử mẹ hợp nhất tất cả các phần tử đó:

Mục danh sách có hình ảnh, một số văn bản và biểu tượng dấu trang
Hình 3. Mục danh sách có hình ảnh, một số văn bản và biểu tượng dấu trang.

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

Khi người dùng nhấn vào mục clickable Row, bài viết sẽ mở ra. Bên trong có một BookmarkButton để đánh dấu bài viết. Nút lồng nhau này xuất hiện dưới dạng chưa hợp nhất, trong khi phần còn lại của nội dung con bên trong hàng được hợp nhất:

Cây hợp nhất chứa nhiều văn bản trong một danh sách bên trong nút Hàng. Cây chưa hợp nhất chứa các nút riêng cho mỗi thành phần kết hợp bằng Văn bản.
Hình 4. Cây hợp nhất chứa nhiều văn bản trong một danh sách bên trong nút Row. Cây chưa hợp nhất chứa các nút riêng cho mỗi Text composable.

Theo thiết kế, một số thành phần kết hợp không được tự động hợp nhất trong một thành phần mẹ. Thành phần mẹ không thể hợp nhất các thành phần con khi các thành phần con cũng đang hợp nhất, dù là do đặt mergeDescendants = true một cách rõ ràng hay do là các thành phần tự hợp nhất, chẳng hạn như nút hoặc thành phần có thể nhấp. Việc biết cách một số API hợp nhất hoặc không hợp nhất có thể giúp bạn gỡ lỗi một số hành vi có thể không mong muốn.

Sử dụng tính năng hợp nhất khi các phần tử con tạo thành một nhóm hợp lý và hợp lý trong phần tử mẹ. Tuy nhiên, nếu các thành phần con lồng nhau cần điều chỉnh hoặc xoá ngữ nghĩa theo cách thủ công, thì các API khác có thể phù hợp hơn với nhu cầu của bạn (ví dụ: clearAndSetSemantics).

Xoá và đặt ngữ nghĩa

Nếu cần xoá hoặc ghi đè hoàn toàn thông tin ngữ nghĩa, bạn có thể sử dụng một API mạnh mẽ là clearAndSetSemantics.

Khi một thành phần cần xoá ngữ nghĩa của chính nó và ngữ nghĩa con, hãy sử dụng API này với một lambda trống. Khi ngữ nghĩa của thành phần kết hợp phải được ghi đè, hãy đưa nội dung mới vào bên trong lambda.

Xin lưu ý rằng khi xoá bằng một lambda trống, ngữ nghĩa đã xoá sẽ không được gửi đến bất kỳ người dùng nào sử dụng thông tin này, chẳng hạn như hỗ trợ tiếp cận, tự động điền hoặc kiểm thử. Khi ghi đè nội dung bằng clearAndSetSemantics{/*semantic information*/}, ngữ nghĩa mới sẽ thay thế tất cả ngữ nghĩa trước đó của phần tử và các phần tử con cháu của phần tử đó.

Sau đây là ví dụ về thành phần bật/tắt tuỳ chỉnh, được biểu thị bằng một hàng có thể tương tác có biểu tượng và văn bản:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

Mặc dù biểu tượng và văn bản có một số thông tin ngữ nghĩa, nhưng chúng không cho biết thành phần này có thể bật/tắt. Việc hợp nhất là chưa đủ vì bạn phải cung cấp thêm thông tin về thành phần.

Vì đoạn mã trên tạo một thành phần bật/tắt tuỳ chỉnh, nên bạn cần thêm khả năng bật/tắt cũng như ngữ nghĩa stateDescription, toggleableState và role. Bằng cách này, trạng thái thành phần và hành động liên quan sẽ có sẵn, ví dụ: TalkBack thông báo "Nhấn đúp để bật/tắt" thay vì "Nhấn đúp để kích hoạt".

Bằng cách xoá ngữ nghĩa ban đầu và đặt ngữ nghĩa mới, mang tính mô tả hơn, các dịch vụ hỗ trợ tiếp cận hiện có thể thấy đây là một thành phần có thể bật/tắt và có thể thay đổi trạng thái.

Khi sử dụng clearAndSetSemantics, hãy cân nhắc những điều sau:

  • Vì các dịch vụ không nhận được thông tin khi API này được đặt, nên bạn chỉ nên sử dụng API này một cách hạn chế.
    • Thông tin ngữ nghĩa có thể được các tác nhân AI và các dịch vụ tương tự sử dụng để hiểu màn hình, do đó chỉ nên xoá khi cần thiết.
  • Bạn có thể đặt ngữ nghĩa tuỳ chỉnh trong lambda API.
  • Thứ tự của đối tượng sửa đổi rất quan trọng – API này xoá tất cả ngữ nghĩa sau vị trí áp dụng, bất kể các chiến lược hợp nhất khác.

Ẩn ngữ nghĩa

Trong một số trường hợp, các phần tử không cần được gửi đến dịch vụ hỗ trợ tiếp cận, có thể là do thông tin bổ sung của các phần tử đó không cần thiết cho tính năng hỗ trợ tiếp cận, hoặc các phần tử đó chỉ mang tính trang trí trực quan và không tương tác. Trong những trường hợp này, bạn có thể ẩn các phần tử bằng API hideFromAccessibility.

Trong các ví dụ sau, có những thành phần có thể cần được ẩn: hình mờ thừa trải dài trên một thành phần và một ký tự dùng để phân tách thông tin theo cách trang trí.

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

Việc sử dụng hideFromAccessibility ở đây đảm bảo hình mờ và thành phần trang trí bị ẩn khỏi các dịch vụ hỗ trợ tiếp cận, nhưng vẫn giữ ngữ nghĩa cho các trường hợp sử dụng khác, chẳng hạn như kiểm thử.

Thông tin chi tiết về trường hợp sử dụng

Sau đây là thông tin tóm tắt về các trường hợp sử dụng để hiểu cách phân biệt rõ ràng giữa các API trước đó:

  • Khi nội dung không được dùng cho các dịch vụ hỗ trợ tiếp cận:
    • Sử dụng hideFromAccessibility khi nội dung có thể mang tính trang trí hoặc thừa, nhưng vẫn phải được kiểm tra.
    • Sử dụng clearAndSetSemantics{} với một lambda trống khi cần xoá ngữ nghĩa của thành phần mẹ và thành phần con cho tất cả các dịch vụ.
    • Sử dụng clearAndSetSemantics{/*content*/} với nội dung bên trong lambda khi ngữ nghĩa của thành phần cần được đặt theo cách thủ công.
  • Khi nội dung cần được coi là một thực thể và cần tất cả thông tin của các phần tử con để hoàn tất:
    • Sử dụng thành phần con ngữ nghĩa hợp nhất.
Bảng có các trường hợp sử dụng API riêng biệt.
Hình 5. Bảng trình bày các trường hợp sử dụng API riêng biệt.