合併和清除

當無障礙服務瀏覽畫面上的元素時,請務必以適當的精細度將這些元素分組、分隔,甚至隱藏。如果畫面上的每個低階可組合項各自醒目顯示,使用者就必須互動許多次才能在畫面上移動。如果元素過度合併,使用者可能無法理解哪些元素在邏輯上屬於同一群組。如果畫面上有純裝飾性的元素,這些元素可能會隱藏在無障礙服務中。在這種情況下,您可以使用 Compose API 合併、清除和隱藏語義。

合併語意

clickable 修飾符套用至父項可組合項時,Compose 會自動合併其下的所有子項元素。如要瞭解互動式 Compose Material 和 Foundation 元件如何根據預設使用合併策略,請參閱「互動式元素」一節。

元件通常由多個可組合項組成。這些可組合項可能會形成邏輯群組,且每個可組合項都可能包含重要資訊,但您仍可能希望無障礙服務將其視為單一元素。

舉例來說,假設可組合項會顯示使用者的顯示圖片、使用者名稱和一些額外資訊:

一組 UI 元素,包括使用者名稱。已選取名稱。
圖 1. 一組 UI 元素,包括使用者名稱。已選取名稱。

您可以在語意輔助鍵中使用 mergeDescendants 參數,讓 Compose 合併這些元素。這樣一來,無障礙服務會將元件視為一個實體,並合併子系的所有語意屬性:

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date}${metadata.readTimeMinutes} min read")
        }
    }
}

無障礙服務現在一次聚焦於整個容器,並合併其內容:

一組 UI 元素,包括使用者名稱。所有元素一併選取。
圖 2. 一組 UI 元素,包括使用者名稱。所有元素一併選取。

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

在其他情況下,您可能會希望子語義合併至父語義,但實際上並未發生。在以下範例中,我們有含有子項元素的 clickable 清單項目父項,且我們可能會預期父項會合併所有子項:

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

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

當使用者按下 clickable 項目 Row 時,系統會開啟文章。內嵌其中的 BookmarkButton 可用來為文章加上書籤。這個巢狀按鈕會顯示為未合併,而列中其餘的子項內容則會合併:

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

根據設計,部分可組合項不會自動在父項下合併。當子項也正在合併時,父項就無法合併子項,無論是明確設定 mergeDescendants = true,或是透過可自行合併的元件 (例如按鈕或可點選項目) 合併,瞭解特定 API 的合併方式或不支援合併的情況,有助於您偵錯一些可能發生的異常行為。

當子項元素在父項下構成邏輯且合理的群組時,請使用合併功能。不過,如果巢狀子項需要手動調整或移除自己的語意,其他 API 可能更符合您的需求 (例如 clearAndSetSemantics)。

清除及設定語意

如果需要完全清除或覆寫語意資訊,可以使用功能強大的 clearAndSetSemantics API。

如果元件需要清除自身和子項的語意,請使用此 API 搭配空 lambda。當必須覆寫其語意時,請在 lambda 中加入新內容。

請注意,使用空 lambda 清除時,系統不會將清除的語意傳送給使用這類資訊的任何使用者,例如無障礙功能、自動填入或測試。使用 clearAndSetSemantics{/*semantic information*/} 覆寫內容時,新語意會取代元素及其子系的所有舊語意。

以下是自訂切換按鈕元件的示例,以含有圖示和文字的可互動資料列表示:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

雖然圖示和文字含有某些語意資訊,但兩者並未共同表示這個元件可切換。您必須提供元件的其他資訊,因此合併是不夠的。

由於上述程式碼片段會建立自訂切換元件,因此您需要新增切換功能,以及 stateDescriptiontoggleableStaterole 語意。這樣一來,您就能使用元件狀態和相關聯的動作,例如 TalkBack 會朗讀「輕觸兩下即可切換」而非「輕觸兩下即可啟用」。

只要清除原始語意並設定更具描述性的新語意,無障礙服務現在就能瞭解這是可切換狀態的切換元件。

使用 clearAndSetSemantics 時,請考量下列事項:

  • 由於服務在設定此 API 時不會收到任何資訊,因此建議您謹慎使用。
    • AI 服務專員和類似服務可能會使用語意資訊來瞭解螢幕,因此只有在必要時才應清除。
  • 您可以在 API lambda 中設定自訂語意。
  • 修飾符的順序很重要,這個 API 會清除套用修飾符後的所有語意,不論其他合併策略為何。

隱藏語意

在某些情況下,元素不需要傳送至無障礙服務,因為其額外資訊對無障礙功能來說可能多餘,或是純粹是視覺裝飾性元素,且不具互動性。在這種情況下,您可以使用 hideFromAccessibility API 隱藏元素。

以下範例顯示可能需要隱藏的元件:跨越元件的多餘浮水印,以及用於裝飾性區隔資訊的字元。

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

在此使用 hideFromAccessibility 可確保水印和裝飾圖案不會顯示在無障礙服務中,但仍可保留其語意,供其他用途使用,例如測試。

用途細目

以下是用途摘要,可協助您瞭解如何清楚區分先前的 API:

  • 當內容不供無障礙服務使用時:
    • 如果內容可能只是裝飾性或重複性內容,但仍須測試,請使用 hideFromAccessibility
    • 如需為所有服務清除父項和子項語意,請使用 clearAndSetSemantics{} 搭配空 lambda。
    • 當需要手動設定元件的語意時,請在 lambda 內使用 clearAndSetSemantics{/*content*/} 與內容。
  • 內容應視為一個實體,且需要所有子項的資訊才能完整:
    • 使用合併語意子項。
表格:API 的不同用途。
圖 5. 表格:列出不同 API 的用途。