Compose 的語義

組合可用來描述應用程式的 UI,可透過執行可組合項產生。組合是一種樹狀結構,由描述您的 UI 的可組合函式組成。

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

一般 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. 切換按鈕,處於「開啟」和「關閉」狀態。

如要描述這個元素的含義,您可以說:"這是一個開關,這是處於「On」狀態的可切換元素。只要按一下即可與其互動。」

這就是語義屬性的用途。這個 Switch 元素的語意節點包含下列屬性,如版面配置檢查器所示:

版面配置檢查器顯示 Switch 可組合項的 Semantics 屬性
圖 4. 版面配置檢查器顯示 Switch 可組合項的 Semantics 屬性。

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

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

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

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

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

這類可組合項的範例便是 Button。一個按鈕可以視為單一元素,即使它可能包含多個子節點:

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

在 Semantics 樹狀圖中,該按鈕的子系屬性會合併,且按鈕在樹狀結構中會顯示為單一分葉節點:

合併的單一分葉語意表示法
圖 5. 合併的單一分葉語意表示法。

可組合元件和修飾元可透過呼叫 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")

版面配置檢查器可讓您在檢視畫面篩選器中選取偏好的樹狀圖,藉此顯示合併和未合併的 Semantics 樹狀圖:

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

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

語意屬性已合併及設定
圖 7:語意屬性已合併及設定。

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

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

如要覆寫這個行為,請將比對器的 useUnmergedTree 參數設為 true,做法與 onRoot 比對器相同。

合併行為

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

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

重要的是,本身已設定 mergeDescendants = true 的子係不會納入合併作業中。範例如下:

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

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

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

調整語意樹狀圖

如前所述,您可以覆寫或清除特定語意屬性,或是變更樹狀結構的合併行為。當您要建立自訂元件時,這一點尤其重要。如果沒有設定正確的屬性和合併行為,您的應用程式可能會無法存取,而且測試的行為可能會與您預期的不同。如果想進一步瞭解在哪些常見用途中應調整語意樹狀圖,請參閱無障礙功能說明文件。如要進一步瞭解測試,請參閱測試指南

其他資源