語義

除了可組合項攜帶的主要資訊 (例如 Text 可組合項的文字字串) 之外,提供更多有關 UI 元素的補充資訊也很有幫助。

Compose 中元件的意義和角色相關資訊稱為「語意」,可為無障礙、自動填入和測試等服務提供可組合項的額外背景資訊。舉例來說,相機圖示在視覺上可能只是一張圖片,但語意可能是「拍照」。

只要將適當的語意與適當的 Compose API 結合,您就能盡可能提供有關元件的資訊給無障礙服務,然後由無障礙服務決定如何向使用者呈現。

Material 和 Compose UI 與 Foundation API 內建語意,會遵循其特定角色和功能,但您也可以根據特定需求,修改現有 API 的語意,或為自訂元件設定新語意。

語意屬性

語意屬性可傳達對應可組合項的意義。舉例來說,Text 可組合函式包含語意屬性 text,因為這是可組合函式的含義。Icon 包含 contentDescription 屬性 (若由開發人員設定),該屬性透過文字形式提供圖示的含義。

思考語意屬性如何傳達可組合項的意義。假設有一個 Switch。使用者所看到的畫面如下:

圖 1. Switch處於「開啟」和「關閉」狀態。

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

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

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

Role 代表元素類型。StateDescription 說明瞭應該如何參考「開啟」狀態。根據預設,這只是「開啟」一詞的本地化版本,但您可以根據背景資訊提供更明確的字詞 (例如「已啟用」)。ToggleableState 是切換按鈕的目前狀態。OnClick 屬性會參照與這個元素互動的方法。

追蹤應用程式中每個可組合函式的元件資訊,藉此發掘許多強大的功能:

  • 無障礙服務會使用屬性來呈現畫面上顯示的 UI,並允許使用者與其互動。如果是 Switch 可組合項,Talkback 可能會朗讀:「開啟;切換;輕觸兩下即可切換」。使用者只要輕觸兩下螢幕即可關閉切換按鈕。
  • 測試架構會使用屬性尋找節點、與節點互動以及進行宣告:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

根據預設,以 Compose 基礎程式庫為基礎建構的可組合項和修飾符,已為您設定相關屬性。您也可以視需要手動變更這些屬性,改善特定用途的無障礙功能支援,或變更可組合項的合併或清除策略。

如要向無障礙服務指出元件的特定內容類型,您可以套用各種不同的語意。這些新增內容將支援現有的主要語意資訊,並協助無障礙服務微調元件的呈現、宣告或互動方式。

如需完整的語意屬性清單,請參閱 SemanticsProperties 物件。如需完整的無障礙操作清單,請參閱 SemanticsActions 物件。

標題

應用程式通常包含文字豐富的畫面,例如長篇文章或新聞頁面,這些內容通常會以標題分成不同子區段:

網誌文章,且文章文字位於可捲動的容器中。
圖 3. 網誌文章,且文章文字位於可捲動的容器中。

無障礙功能的使用者無法順利瀏覽這類畫面。為改善瀏覽體驗,部分無障礙服務可讓使用者更輕鬆地直接在各個區段或標題之間瀏覽。如要啟用這項功能,請定義語意屬性,指出元件為 heading:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

快訊和彈出式視窗

如果元件是快訊或彈出式視窗 (例如 Snackbar),您可能需要向無障礙服務發出信號,表示可以向使用者傳達新結構或內容更新。

類似快訊的元件可標示 liveRegion 語意屬性。這樣一來,無障礙服務就能自動通知使用者這個元件或其子項的變更:

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

在大多數情況下,如果使用者只需短暫注意螢幕上的快訊或重要變更內容,請使用 liveRegionMode.Polite。

請謹慎使用 liveRegion.Assertive,以免造成干擾。如果必須讓使用者知道內容有時效性,請使用這個屬性:

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

請勿在頻繁更新的內容 (例如倒數計時器) 上使用即時區域,以免使用者因不斷收到回饋而感到困擾。

類似視窗的元件

類似視窗的自訂元件 (類似 ModalBottomSheet) 需要額外信號,才能與周圍內容區分。為此,您可以使用 paneTitle 語意,讓無障礙服務能適當呈現任何相關視窗或窗格變更,以及其主要語意資訊:

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

如需參考資料,請參閱 Material 3 如何將 paneTitle 用於其元件。

錯誤元件

如果是其他內容類型 (例如類似錯誤的元件),您可能需要為有無障礙需求的使用者展開主要語意資訊。定義錯誤狀態時,您可以向無障礙服務告知其 error 語意,並提供展開的錯誤訊息。

在這個範例中,TalkBack 會朗讀主要錯誤文字資訊,接著朗讀其他展開的訊息:

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

進度追蹤元件

如果是追蹤進度的自訂元件,您可能需要通知使用者進度變更,包括目前的進度值、範圍和步距。您可以使用 progressBarRangeInfo 語意,確保無障礙服務能掌握進度變化,並據此更新使用者。不同的輔助技術也可能有獨特的方式,提示進度增加和減少。

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

清單和項目資訊

在包含多個項目的自訂清單和格線中,無障礙服務可能也需要接收更詳細的資訊,例如項目和索引總數。

在清單和項目上分別使用 collectionInfo 和 collectionItemInfo 語意,在這個長清單中,無障礙服務除了文字語意資訊外,還能告知使用者他們在整個集合中的項目索引:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

狀態說明

可組合項可定義語意的 stateDescription,供 Android 架構用來解讀可組合項所處的狀態。舉例來說,可切換的可組合項可能處於「已勾選」或「未勾選」狀態。在某些情況下,您可能會想覆寫 Compose 使用的預設狀態說明標籤。您可以先明確指定狀態說明標籤,再將可組合項定義為可切換:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

自訂操作

自訂動作可用於更複雜的觸控螢幕手勢,例如滑動關閉或拖放,因為這些手勢對於有運動障礙或其他身心障礙的使用者來說,可能難以互動。

如要讓滑動關閉手勢更易於使用,您可以將其連結至自訂動作,並傳遞關閉動作和標籤:

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

TalkBack 等無障礙服務會醒目顯示該元件,並提示選單中還有更多動作,代表滑動關閉動作:

TalkBack 動作選單示意圖
圖 4. TalkBack 動作選單示意圖。

自訂動作的另一個用途是,當清單很長且項目有更多可用動作時,使用者可能需要逐一瀏覽每個項目的每個動作,這會很繁瑣:

=切換控制功能在畫面上的導覽示意圖
圖 5. 螢幕上的切換控制功能導覽示意圖。

如要改善導覽體驗,特別是針對切換控制功能或 Voice Access 等互動式輔助技術,您可以在容器上使用自訂動作,將動作移出個別周遊,並移至獨立的動作選單:

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

在這種情況下,請務必使用 clearAndSetSemantics 修飾符手動清除原始子項的語意,因為您要將這些子項移至自訂動作。

以切換控制功能為例,選取容器後,系統會開啟選單,並列出可用的巢狀動作:

文章清單項目的切換控制功能醒目顯示
圖 6. 文章清單項目的切換控制功能醒目顯示。
切換控制功能動作選單的示意圖。
圖 7. 切換控制功能動作選單的示意圖。

語意樹

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

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

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

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

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

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

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

合併及未合併的樹狀結構

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

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

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

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

合併的單一葉語意表示法
圖 10. 合併的單一葉節點語意表示法。

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

Foundation 和 Material Compose 程式庫中的數個修飾符和可組合項具有該屬性設定。例如,clickable 和 toggleable 修飾元會自動合併它們的子系。此外,ListItem 可組合元件也會合併其子系。

檢查樹狀圖

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

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

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

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

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

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

合併及設定語意屬性
圖 12. 合併並設定語意屬性。

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

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

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

調整樹狀圖

如上所述,您可以覆寫或清除特定語意屬性,或變更樹狀結構的合併行為。當您建立自訂可組合元件時,這一點尤其重要。如果沒有設定正確的屬性和合併行為,您的應用程式可能會無法存取,而測試行為可能與您預期的不同。如要進一步瞭解測試,請參閱測試指南。