Ngữ nghĩa trong Compose

Thành phần kết hợp mô tả giao diện người dùng của ứng dụng và được tạo ra bằng cách chạy các thành phần kết hợp. Thành phần kết hợp là một cấu trúc dạng cây bao gồm các thành phần kết hợp mô tả giao diện người dùng.

Bên cạnh Cấu trúc, có một cây song song, có tên là cây ngữ nghĩa. Cây này mô tả giao diện người dùng của bạn theo một cách khác mà các dịch vụ Hỗ trợ tiếp cận và khung Kiểm thử có thể hiểu được. Các dịch vụ hỗ trợ tiếp cận sử dụng cây để mô tả ứng dụng cho những người dùng có nhu cầu cụ thể. Khung kiểm thử sử dụng cây để tương tác với ứng dụng của bạn và đưa ra nhận định về ứng dụng đó. Cây Ngữ nghĩa không chứa thông tin về cách vẽ thành phần kết hợp, nhưng có chứa thông tin về ngữ nghĩa ý nghĩa của các thành phần kết hợp.

Một hệ phân cấp giao diện người dùng điển hình và cây ngữ nghĩa
Hình 1. Một hệ phân cấp giao diện người dùng điển hình và cây ngữ nghĩa.

Nếu ứng dụng của bạn bao gồm các thành phần kết hợp và đối tượng sửa đổi từ nền tảng Compose và thư viện tài liệu, thì cây ngữ nghĩa sẽ được tự động điền và tạo cho bạn. Tuy nhiên, khi thêm các thành phần kết hợp tuỳ chỉnh cấp thấp, bạn phải cung cấp ngữ nghĩa theo cách thủ công. Cũng có thể có các trường hợp mà cây của bạn không thể hiện chính xác hoặc đầy đủ ý nghĩa của các phần tử trên màn hình, trong trường hợp đó, bạn có thể điều chỉnh cây.

Xem xét ví dụ về thành phần kết hợp lịch tuỳ chỉnh này:

Thành phần kết hợp tuỳ chỉnh cho lịch có các phần tử ngày có thể chọn
Hình 2. Một thành phần kết hợp lịch tuỳ chỉnh có các phần tử ngày có thể chọn.

Trong ví dụ này, toàn bộ lịch được triển khai dưới dạng thành phần kết hợp cấp thấp, bằng cách sử dụng Layout có thể kết hợp và vẽ trực tiếp vào Canvas. Nếu bạn không làm gì khác, các dịch vụ hỗ trợ tiếp cận sẽ không nhận được đủ thông tin về nội dung của thành phần kết hợp và lựa chọn của người dùng trong lịch. Ví dụ: nếu người dùng nhấp vào ngày chứa 17, khung hỗ trợ tiếp cận sẽ chỉ nhận được thông tin mô tả cho toàn bộ quyền kiểm soát lịch. Trong trường hợp này, dịch vụ hỗ trợ tiếp cận TalkBack sẽ thông báo "Lịch" hoặc tốt hơn một chút là "Lịch tháng Tư" và người dùng sẽ vẫn còn thắc mắc về ngày đã chọn. Để thành phần kết hợp này dễ tiếp cận hơn, bạn cần thêm thông tin ngữ nghĩa theo cách thủ công.

Thuộc tính ngữ nghĩa

Tất cả các nút trong cây giao diện người dùng có một số ý nghĩa ngữ nghĩa đều có một nút song song trong cây Ngữ nghĩa. Nút trong cây Ngữ nghĩa chứa các thuộc tính truyền tải ý nghĩa của thành phần kết hợp tương ứng. Ví dụ: thành phần kết hợp Text chứa một thuộc tính ngữ nghĩa text, vì đó là ý nghĩa của thành phần kết hợp đó. Icon chứa thuộc tính contentDescription (nếu do nhà phát triển đặt) để truyền tải ý nghĩa của Icon dưới dạng văn bản. Các thành phần kết hợp và đối tượng sửa đổi được tạo dựa trên thư viện nền tảng Compose đã thiết lập cho bạn các thuộc tính liên quan. Bạn có thể đặt hoặc ghi đè các thuộc tính bằng đối tượng sửa đổi semanticsclearAndSetSemantics (không bắt buộc). Ví dụ: thêm các thao tác hỗ trợ tiếp cận tuỳ chỉnh vào một nút, cung cấp nội dung mô tả trạng thái thay thế cho một phần tử có thể bật/tắt hoặc cho biết rằng nên cân nhắc một thành phần kết hợp văn bản nhất định là một tiêu đề.

Để trực quan hoá cây Ngữ nghĩa, hãy sử dụng Công cụ Layout Inspector hoặc sử dụng phương thức printToLog() bên trong kiểm thử. Thao tác này sẽ in cây Ngữ nghĩa hiện tại bên trong Logcat.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

Kết quả của lần kiểm thử này sẽ là:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Xem xét cách các thuộc tính ngữ nghĩa truyền tải ý nghĩa của một thành phần kết hợp. Hãy cân nhắc dùng Switch. Đây là giao diện mà người dùng thấy:

Hình 3. Nút chuyển ở trạng thái "Bật" và "Tắt".

Để mô tả ý nghĩa của phần tử này, bạn có thể nói như sau: "Đây là Nút chuyển, là một phần tử có thể bật/tắt ở trạng thái "Bật". Bạn có thể nhấp vào để tương tác với công cụ này."

Đây chính xác là những thuộc tính ngữ nghĩa được dùng. Nút ngữ nghĩa của phần tử Chuyển đổi này chứa các thuộc tính sau, như được trực quan hoá bằng Layout Inspector:

Layout Inspector cho thấy các thuộc tính ngữ nghĩa của thành phần kết hợp Switch (Chuyển đổi)
Hình 4. Layout Inspector cho thấy các thuộc tính ngữ nghĩa của thành phần kết hợp Switch (Chuyển đổi).

Role cho biết loại phần tử. StateDescription mô tả cách tham chiếu trạng thái "Bật". Theo mặc định, đây là phiên bản đã bản địa hoá của từ "On" (Bật), nhưng bạn có thể tạo phiên bản cụ thể hơn (ví dụ: "Enabled") dựa trên ngữ cảnh. ToggleableState là trạng thái hiện tại của Nút chuyển. Thuộc tính OnClick tham chiếu phương thức dùng để tương tác với phần tử này. Để xem danh sách đầy đủ các thuộc tính ngữ nghĩa, hãy xem đối tượng SemanticsProperties. Để biết danh sách đầy đủ các Hành động hỗ trợ tiếp cận, hãy xem đối tượng SemanticsActions.

Việc theo dõi các thuộc tính ngữ nghĩa của từng thành phần kết hợp trong ứng dụng của bạn sẽ mở ra rất nhiều khả năng mạnh mẽ. Một số ví dụ:

  • TalkBack sử dụng các thuộc tính để đọc to nội dung hiển thị trên màn hình và cho phép người dùng tương tác suôn sẻ với nội dung đó. Đối với thành phần kết hợp Nút chuyển, TalkBack có thể nói: "Bật; Chuyển; nhấn đúp để bật". Người dùng có thể nhấn đúp vào màn hình để tắt nút chuyển.
  • Khung kiểm thử sử dụng các thuộc tính để tìm nút, tương tác với chúng và đưa ra xác nhận. Quy trình kiểm thử mẫu cho Nút chuyển có thể là:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Cây Ngữ nghĩa đã hợp nhất và chưa hợp nhất

Như đã đề cập trước đó, mỗi thành phần kết hợp trong cây Giao diện người dùng có thể không có hoặc có nhiều thuộc tính ngữ nghĩa. Khi chưa đặt thuộc tính ngữ nghĩa, một thành phần kết hợp sẽ không được đưa vào cây Ngữ nghĩa. Bằng cách đó, cây Ngữ nghĩa chỉ chứa các nút thực sự chứa ý nghĩa ngữ nghĩa. Tuy nhiên, đôi khi, để truyền đạt ý nghĩa chính xác của nội dung hiển thị trên màn hình, bạn cũng nên hợp nhất một số cây phụ của nút và coi chúng là một. Bằng cách đó, bạn có thể giải thích về toàn bộ tập hợp nút, thay vì xử lý từng nút con riêng lẻ. Theo quy tắc chung, mỗi nút trong cây này đại diện cho một phần tử có thể lấy tiêu điểm khi sử dụng các dịch vụ Hỗ trợ tiếp cận.

Ví dụ về thành phần kết hợp như vậy là Button. Bạn có thể lập luận về việc nút là một phần tử duy nhất, mặc dù nút đó có thể chứa nhiều nút con:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Trong cây Ngữ nghĩa, các thuộc tính của các thành phần con cháu của nút được hợp nhất và nút này được biểu thị dưới dạng một nút lá đơn trong cây:

Biểu diễn ngữ nghĩa một lá hợp nhất
Hình 5. Hợp nhất cách biểu diễn ngữ nghĩa một lá.

Các thành phần kết hợp và đối tượng sửa đổi có thể cho biết rằng chúng muốn hợp nhất các thuộc tính ngữ nghĩa của phần tử con bằng cách gọi Modifier.semantics (mergeDescendants = true) {}. Việc đặt thuộc tính này thành true cho biết rằng thuộc tính ngữ nghĩa cần được hợp nhất. Trong ví dụ về Button, thành phần kết hợp Button sử dụng đối tượng sửa đổi clickable nội bộ bao gồm đối tượng sửa đổi semantics này. Do đó, các nút con cháu của nút được hợp nhất. Hãy đọc tài liệu về khả năng hỗ trợ tiếp cận để tìm hiểu thêm về thời điểm bạn nên thay đổi hành vi hợp nhất trong thành phần kết hợp.

Một số đối tượng sửa đổi và thành phần kết hợp trong Thư viện nền tảng và thư viện Material Compose có bộ thuộc tính này. Ví dụ: đối tượng sửa đổi clickabletoggleable sẽ tự động hợp nhất các phần tử con. Ngoài ra, ListItem hoạt động tương ứng sẽ hợp nhất các thành phần con.

Kiểm tra cây

Thực tế, cây ngữ nghĩa là hai cây khác nhau. Có một cây Ngữ nghĩa hợp nhất, hợp nhất các nút con cháu khi bạn đặt mergeDescendants thành true. Ngoài ra còn có một cây Ngữ nghĩa chưa hợp nhất không áp dụng tính năng hợp nhất, nhưng giữ nguyên mọi nút. Các dịch vụ hỗ trợ tiếp cận sử dụng cây chưa hợp nhất và áp dụng thuật toán hợp nhất của riêng dịch vụ đó, có tính đến thuộc tính mergeDescendants. Theo mặc định, khung kiểm thử sử dụng cây đã hợp nhất.

Bạn có thể kiểm tra cả hai cây bằng phương thức printToLog(). Theo mặc định, như trong các ví dụ trước, cây đã hợp nhất sẽ được ghi lại. Để in cây chưa hợp nhất, hãy đặt tham số useUnmergedTree của trình so khớp onRoot() thành true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Layout Inspector cho phép bạn hiển thị cả cây Ngữ nghĩa hợp nhất và chưa hợp nhất bằng cách chọn một cây ưu tiên trong bộ lọc khung hiển thị:

Các tuỳ chọn khung hiển thị của Layout Inspector, cho phép hiển thị cả cây ngữ nghĩa hợp nhất và chưa hợp nhất
Hình 6. Các tuỳ chọn khung hiển thị của Layout Inspector, cho phép hiển thị cả cây ngữ nghĩa hợp nhất và chưa hợp nhất.

Đối với mỗi nút trong cây của bạn, Layout Inspector hiển thị cả Ngữ nghĩa hợp nhất và Ngữ nghĩa chưa hợp nhất đã đặt trên nút đó trong bảng thuộc tính:

Thuộc tính ngữ nghĩa được hợp nhất và tập hợp
Hình 7. Thuộc tính ngữ nghĩa được hợp nhất và đặt.

Theo mặc định, các trình so khớp trong Khung kiểm thử sẽ sử dụng cây Ngữ nghĩa đã hợp nhất. Đó là lý do bạn có thể tương tác với Button bằng cách so khớp văn bản xuất hiện bên trong Button đó:

composeTestRule.onNodeWithText("Like").performClick()

Hãy ghi đè hành vi này bằng cách đặt tham số useUnmergedTree của trình so khớp thành true, tương tự như với trình so khớp onRoot.

Hành vi hợp nhất

Khi một thành phần kết hợp cho biết rằng các thành phần con của nó nên được hợp nhất, thì việc hợp nhất này xảy ra chính xác như thế nào?

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ị ContentDescription con vào một danh sách. 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 việc 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ị mẹ hoặc con, hợp nhất các giá trị thành một danh sách hoặc chuỗi, hoàn toàn không cho phép hợp nhất và gửi ngoại lệ hoặc bất kỳ chiến lược hợp nhất tuỳ chỉnh nào khác.

Một lưu ý quan trọng là các thành phần con cháu đã đặt mergeDescendants = true sẽ không được đưa vào quá trình hợp nhất. Hãy xem ví dụ:

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 8. Mục danh sách có hình ảnh, một số văn bản và biểu tượng dấu trang.

Dưới đây là một mục danh sách có thể nhấp. Khi người dùng nhấn vào hàng, ứng dụng sẽ chuyển đến trang chi tiết bài viết, nơi người dùng có thể đọc bài viết. Bên trong mục danh sách, có một nút để đánh dấu bài viết này. Nút này tạo thành một phần tử có thể nhấp được lồng nhau, do đó nút này sẽ xuất hiện riêng biệt trong cây đã hợp nhất. Nội dung còn lại 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 9. 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 Text (Văn bản).

Điều chỉnh cây Ngữ nghĩa

Như đã đề cập trước đó, bạn có thể ghi đè hoặc xoá một số thuộc tính ngữ nghĩa hoặc thay đổi hành vi hợp nhất của cây. Điều này đặc biệt cần thiết khi bạn tạo các thành phần tuỳ chỉnh của riêng mình. Nếu không đặt đúng thuộc tính và hành vi hợp nhất, ứng dụng của bạn có thể không truy cập được và các hoạt động kiểm thử có thể hoạt động khác với mong đợi. Để đọc thêm về một số trường hợp sử dụng phổ biến mà bạn nên điều chỉnh cây Ngữ nghĩa, hãy đọc tài liệu về hỗ trợ tiếp cận. Nếu bạn muốn tìm hiểu thêm về quy trình kiểm thử, hãy xem hướng dẫn kiểm thử.

Tài nguyên khác