Compose 的語義

Composition 描述了應用程式的 UI,可透過執行各個可組合元件產生。Composition 是一種樹狀結構,內含可描述 UI 的可組合元件。

Composition 旁邊有一個平行樹狀結構,也稱為 語義樹狀圖。這個樹狀圖以 無障礙 服務和 測試 架構能夠理解的替代方式描述 UI。無障礙服務會使用樹狀圖向有特定需求的使用者描述該應用程式。測試架構會使用樹狀圖與您的應用程式互動,並對其做出判斷提示。語義樹狀圖未包含如何 繪製 可組合元件的資訊,但包含了可組合元件的 語義含義 的資訊。

圖 1. 一般 UI 階層及其語意樹狀結構。

如果您的應用程式是由 Compose 基礎和材質庫的可組合元件和修飾元組成,系統會自動為您產生並填入語義樹狀圖。不過,當您新增自訂低層級可組合元件時,就必須手動提供其語意。在某些情況下,您的樹狀結構可能無法正確或完整反映螢幕上的元素含義,這時您只要調整樹狀結構即可。

以這個自訂日曆可組合元件為例:

圖 2. 包含可選取日可組合元件的自訂日曆。

在這個例子中,系統會使用Layout可組合元件且直接繪製入Canvas,從而將整個日曆作為單一低層級元件納入其中。如果您不採取任何其他行動,無障礙服務將無法收到有關可組合元件內容的資訊,也無法收到使用者在日曆中的選取資訊。舉例來說,如果使用者點擊包含 17 的日期,無障礙架構只會接收整個日曆控制項的說明資訊。在這種情況下,TalkBack 無障礙功能服務只會說出「日曆」音訊,較好的情況下會說出「四月份日曆」,而使用者無法知道到底選取了哪一天。如要讓這元件更容易存取,您必須手動新增語義資訊。

語義屬性

UI 樹狀圖中的所有節點都具有語義含義,這些節點在語義樹狀圖中具有平行節點。語義樹狀圖中的節點所包含的屬性能夠傳達對應可組合元件的意義。舉例來說,Text 可組合元件包含語義屬性 text,因為這是可組合元件的 含義Icon 包含 contentDescription 屬性 (若由開發人員設定),該屬性透過文字形式提供 Icon 的含義。建立在 Compose 基礎程式庫之上的可組合元件和修飾詞已經為您設定了相關屬性。或者,您也可以使用 semanticsclearAndSetSemantics 修飾詞自行設定或覆寫屬性。舉例來說,您可以在節點中新增 自訂無障礙動作、為可切換元素提供替代 狀態說明,或是指出某個特定文字可組合元件應被視為 標題

如要呈現語義樹狀圖,請使用 版面配置檢查器工具,或使用測試中的 printToLog() 方法。這會列印出 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")
    }
}

這項測試的輸出內容如下:

    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]

讓我們來看看一個範例,瞭解語意屬性如何用於傳達可組合元件的含義。假設有 Switch。使用者所看到的畫面如下:

圖 3. 表示切換按鈕處於「開啟」和「關閉」狀態。

如要說明這個元素的含義,您可以說:「這是切換按鈕,它是一個可切換元素,目前處於『開啟』狀態。只要按一下即可與其互動。」

這就是語義屬性的用途。這個切換開關元素的語義節點包含下列屬性,用版面配置檢查器可以看到:

圖 4. 版面配置檢查器顯示了切換按鈕可組合元件的語義屬性。

Role 代表我們正在尋找的元素類型。StateDescription 說明了應該如何參考「開啟」狀態。根據預設,這只是「開啟」一詞的本地化版本,但您可以根據背景資訊提供更明確的字詞(例如「已啟用」)。ToggleableState 是切換按鈕的目前狀態。OnClick 屬性會參考與這個元素互動的方法。如需完整的語義屬性清單,請參閱 SemanticsProperties 物件。 如需完整的無障礙操作清單,請參閱 SemanticsActions 物件。

追蹤應用程式中不同元件的語義屬性,藉此發掘許多強大的功能。以下列舉部分範例:

  • Talkback 使用屬性來朗讀螢幕上顯示的內容,讓使用者順利與其互動。我們的切換按鈕可能會顯示為「開啟;切換;輕觸兩下即可切換」。使用者只要輕觸兩下螢幕即可關閉切換按鈕。
  • 測試架構會使用屬性尋找節點、與節點互動以及進行宣告。以下是切換開關的測試範例:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()
    

合併及未合併的語義樹狀圖

如前文所述,UI 樹狀圖中每個可組合元件可能沒有或是有多個語義屬性設定。如果可組合元件沒有語意屬性設定,系統無法將其列入語意樹狀結構。那樣的話,語義樹狀圖僅包含真正含有語義含義的節點。但是,有時為了正確傳遞熒幕上呈現的含義,合併某些節點的子樹系並將其視為一體還是很有幫助的。那樣的話,我們就可以將一組節點當做一個整體進行推理,而非單獨處理每個子節點。根據經驗,在使用無障礙服務時,這個樹狀圖中的每個節點都代表了一個可聚焦元素。

按鈕便是這種可組合元件的一個範例。我們希望將按鈕視為單一元素,即使它可能包含多個子節點:

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

在我們的語義樹狀圖中,按鈕的子系屬性會被合併,且按鈕在樹狀圖中會顯示為單一的分葉節點:

可組合元件和修飾元可透過呼叫 Modifier.semantics (mergeDescendants = true) {} 來合併其子系的語義屬性。如果將這個屬性設定為 true,則表示應該合併語義屬性。在我們的 Button範例中,Button 可組合元件在內部使用了含有 semantics 修飾元的 clickable 修飾元。因此,系統會合併該按鈕的子系節點。請參閱無障礙說明文件,進一步瞭解何時應在可組合元件中 變更合併行為

基礎與資料 Compose 程式庫中的數個修飾元和可組合元件具有該屬性設定。例如,clickabletoggleable 修飾元會自動合併它們的子系。此外,ListItem 可組合元件也會合併其子系。

檢查樹狀圖

談論語義樹狀圖時,我們實際上是在探討兩種不同的樹狀圖。其中一個是 已合併 的語義樹狀圖,該樹狀圖在 mergeDescendants 設定位 true 時合併子系節點。另外還有一個 未合併 的語義樹狀圖,該樹狀圖不會套用合併規則,但會保留所有節點。無障礙服務會使用未合併的樹狀圖並套用自己的合併演算法,並將 mergeDescendants 屬性納入考量。根據預設,測試架構會使用合併的樹狀圖。

您可以使用 printToLog() 方法檢查這兩種樹狀圖。根據預設,和先前的範例一樣,系統會記錄已合併的樹狀圖。如要改為列印未合併的樹狀圖,請將 onRoot() 比對器的 useUnmergedTree 參數設為 true

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

版面配置檢查器可讓您在檢視模式篩選器中選取偏好的樹狀圖,以顯示合併和未合併的語義樹狀圖:

圖 5. 版面配置檢查器檢視選項,允許顯示已合併和未合併的語義樹狀圖。

針對樹狀圖中的每個節點,版面配置檢查器會顯示其合併語義和屬性面板中該節點的語義設定:

根據預設,測試架構中的比對器會使用已合併的語義樹狀圖。因此,您可以透過比對按鈕內顯示的文字來與之互動:

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

如要覆寫此行為,請將比對器的 useUnmergedTree參數設為 true,做法與之前使用 onRoot 比對器相同。

合併行為

當一個可組合元件表示應該合併其子系,那麼這種合併到底是如何進行的?

每個語義屬性都有已定義的合併策略。舉例來說,ContentDescription 屬性會將所有子系 ContentDescription 值新增至清單。如要檢查語義屬性的合併策略,請前往 SemanticsProperties.kt 查看其 mergePolicy 實作項目。屬性選取有以下幾種:總是選取父值或子值、將值合併至清單或字串、完全不允許合併且擲回例外狀況,或任何其他自訂合併策略。

重要的是,本身設定了 mergeDescendants = true 的子系未包含在合併作業中。一起來看看以下範例:

圖 6. 含有圖片、部分文字和書籤圖示的清單項目。

另外還有一個可點擊的清單項目。使用者按一下該列時,應用程式會前往文章詳細資料頁面,使用者便可閱讀文章。清單項目中有一個可用來將文章加入書籤的按鈕。在這個範例中,我們有一個巢狀可點擊元素,因此按鈕會分別顯示在合併的樹狀結構中。該列中其餘內容已被合併:

圖 7. 合併的樹狀圖在列節點內的清單中包含多個文字。未合併的樹狀圖包含每個文字可組合元件的獨立節點。

調整語義樹狀圖

如上所述,您可以覆寫或清除特定語義屬性,或變更樹狀圖的合併行為。當您建立自訂可組合元件時,這一點尤其重要。如果沒有設定正確的屬性和合併行為,您的應用程式可能會無法存取,而測試行為可能與您預期的不同。要閱讀關於在何處調整語義樹狀圖的常見使用案例,請參閱 無障礙說明文件。如要進一步瞭解測試,請查看 測試指南