合併和清除

無障礙服務在瀏覽畫面上的元素時,請務必以適當的精細程度將這些元素分組、分隔或隱藏。如果畫面上的每個低階可組合項各自醒目顯示,使用者就必須互動許多次才能在畫面上移動。如果元素過度合併,問題就變成使用者可能不知道哪些元素屬於同一群組。如果畫面上有純裝飾性的元素,可以對無障礙服務隱藏。在這種情況下,您可以使用 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)。

清除並設定語意

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

當元件需要清除本身和子系的語意時,請使用這個 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?")
    }
}

雖然圖示和文字含有一些語意資訊,但兩者結合後並未指出這個元件可切換。合併是不夠的,因為您必須提供元件的其他資訊。

由於上述程式碼片段會建立自訂切換鈕元件,因此您需要新增切換功能,以及 stateDescription、toggleableState 和 role 語意。這樣一來,系統就會提供元件狀態和相關聯的動作,例如 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。
    • 當需要手動設定元件的語意時,請使用 clearAndSetSemantics{/*content*/},並在 lambda 中加入內容。
  • 當內容應視為單一實體,且需要所有子項的資訊才能完整呈現時:
    • 使用合併語意子系。
表格:不同 API 的用途。
圖 5. 表格:不同 API 的用途。