Ngữ nghĩa trong Compose

Thành phần Compose 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 Compose là một cấu trúc cây bao gồm các thành phần kết hợp mô tả giao diện người dùng của bạn.

Bên cạnh Bản soạn, có một cây 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 cách dễ hiểu đối với các dịch vụ Hỗ trợ tiếp cận và cho khung Kiểm thử. Các dịch vụ hỗ trợ tiếp cận sử dụng cây để mô tả ứng dụng cho người dùng có nhu cầu cụ thể. Khung kiểm thử sử dụng nó để tương tác với ứng dụng của bạn và đưa ra xác nhận về ứng dụng đó. Cây Ngữ nghĩa không chứa thông tin về cách vẽ các thành phần kết hợp của bạn, nhưng có chứa thông tin về ý nghĩa ngữ nghĩa của các thành phần kết hợp.

Hình 1. Một hệ thống 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 cấp thấp tuỳ chỉnh, bạn sẽ 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:

Hình 2. Một thành phần kết hợp lịch tuỳ chỉnh tương ứng với 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 chỉ cần thông báo "Lịch" hoặc chỉ tốt hơn một chút, "Lịch tháng Tư" và người dùng sẽ vẫn còn thắc mắc về ngày đã chọn. Để giúp thành phần kết hợp này dễ dàng 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 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 văn bản về ý nghĩa của Icon. Các thành phần kết hợp và đối tượng sửa đổi được tạo ngoài thư viện nền tảng Compose đã đặt các thuộc tính có liên quan cho bạn. Bạn có thể tuỳ ý đặt hoặc ghi đè thuộc tính bằng các đối tượng sửa đổi semanticsclearAndSetSemantics. Ví dụ: bạn có thể thêm hành động 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ể chuyển đổi hoặc cho biết rằng một thành phần kết hợp bằng văn bản nhất định nên được coi là một tiêu đề.

Để hình ảnh hoá cây Ngữ nghĩa, chúng tôi có thể sử dụng Công cụ Layout Inspector hoặc sử dụng phương thức printToLog() trong các lần kiểm thử của chúng tôi. 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]

Hãy xem ví dụ để biết cách sử dụng thuộc tính ngữ nghĩa để truyền đạt ý nghĩa của một thành phần kết hợp. Hãy nghĩ về một 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ể chuyển đổi, hiện đang ở trạng thái "Bật". Bạn có thể nhấp vào phần này để tương tác với phần 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 hiển thị với Layout Inspector:

Hình 4. Layout Inspector hiển thị các thuộc tính ngữ nghĩa của một thành phần kết hợp Nút chuyển.

Role cho biết loại phần tử mà chúng tôi đang xem xét. StateDescription sẽ mô tả cách tham chiếu trạng thái "Bật". Theo mặc định, đây chỉ là một phiên bản đã bản địa hóa của từ "Bật". Tuy nhiên, phiên bản này có thể được đặt cụ thể hơn (ví dụ: "Đã bật") 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. Để biết 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 Nút chuyển, nó có thể nói: "Đang bật; Chuyển; nhấn đúp để chuyển". Người dùng có thể nhấn đúp vào màn hình để tắt Nút chuyển này.
  • 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. Kiểm thử mẫu cho Nút chuyển của chúng tôi 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 một thành phần kết hợp không đặt thuộc tính ngữ nghĩa, thì tệp đó 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 đó, chúng ta 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.

Một ví dụ về thành phần kết hợp như vậy là Nút. Chúng tôi muốn trình bày về việc nút này 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ủa chúng tôi, các thuộc tính của các thành phần con của Nút được hợp nhất và Nút này được trình bày dưới dạng một nút lá đơn trong cây:

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ụ Button của chúng tôi, 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 của Nút sẽ được hợp nhất. Hãy đọc tài liệu về tính 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 của mình.

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

Khi nói về cây Ngữ nghĩa, chúng tôi đang nói về 2 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 khi mergeDescendants được đặt thành true. Ngoài ra, còn có một cây Ngữ nghĩa chưa được hợp nhất không áp dụng tính năng hợp nhất mà 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 các thuật toán hợp nhất của riêng mình, 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 được 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 ưa thích trong bộ lọc khung hiển thị:

Hình 5. 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:

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. Do đó, bạn có thể tương tác với Nút bằng cách so khớp văn bản hiển thị bên trong nút đó:

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

Bạn có thể ghi đè hành vi này bằng cách đặt tham số useUnmergedTree của trình so khớp thành true, như chúng tôi đã làm trước đây với trình so khớp onRoot.

Đang hợp nhất hành vi

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ị 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ể luôn chọn 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.

Một lưu ý quan trọng là các phần tử con đã đặt mergeDescendants = true không được đưa vào hoạt động hợp nhất. Hãy cùng tham khảo ví dụ dưới đây:

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

Ở đây, chúng tôi có 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 trang bài viết này. Trong trường hợp này, chúng tôi có phần tử có thể nhấp lồng nhau, vì vậy, nút sẽ hiển thị riêng biệt trong cây được hợp nhất. Nội dung còn lại trong hàng được hợp nhất:

Hình 7. 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.

Đ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 phù hợp khi bạn đang 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 lần kiểm thử có thể hoạt động khác với dự kiến. Để đọ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ề kiểm thử, hãy xem Hướng dẫn kiểm thử.