改善 Compose 無障礙功能的重要步驟

為了協助具有無障礙需求的使用者順利使用您的應用程式,您設計應用程式時可支援重要的無障礙功能需求。

考量最低觸控目標大小

任何使用者可點擊、輕觸或進行互動的螢幕元素,都必須設為適當大小,方便使用者進行互動。設定這些元素的大小時,請務必將大小下限設為 48dp,以便正確遵循「質感設計無障礙指南」。

Material Design 元件 (例如 CheckboxRadioButtonSwitchSliderSurface) 可在內部設定這個最小尺寸,但前提是元件可以接收使用者動作。舉例來說,如果 CheckboxonCheckedChange 參數設為非空值,核取方塊會包含邊框間距,其寬度和高度至少為 48 dp。

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

onCheckedChange 參數設為空值時,系統不會納入邊框間距,因為該元件無法直接互動。

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

圖 1. 沒有邊框間距的核取方塊。

導入 SwitchRadioButtonCheckbox 等選取控制項時,您通常會將可點擊的行為推送至父項容器,並將可組合項的點擊回呼設為 null,然後在可組合父項中新增 toggleableselectable 修飾符。

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

如果可點擊的可組合項大小小於最低觸控目標大小,Compose 仍會增加觸控目標大小。方法是將觸控目標大小擴大到可組合項的邊界之外。

以下範例包含非常小的可點擊 Box。觸控目標區域會自動擴展到 Box 的邊界之外,因此輕觸 Box 旁邊的位置仍會觸發點擊事件。

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

為避免不同可組合項的觸控區域重疊,請一律為可組合項使用夠大的最小尺寸。在本範例中,則意味著使用 sizeIn 修飾符設定內部方塊的最小尺寸:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

新增點擊標籤

您可以使用點擊標籤,為可組合項的點擊行為新增語意含義。點擊標籤可說明使用者與可組合項的互動情形。無障礙服務會根據點擊標籤,向有特定需求的使用者描述應用程式。

clickable 修飾符中傳遞參數,藉此設定點擊標籤:

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

如果您無法存取可點擊的修飾詞,請在「semantics」修飾符中設定點擊標籤:

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

描述視覺元素

在您定義 ImageIcon 可組合項時,Android 架構無法自動瞭解應用程式顯示的內容。您必須傳送視覺元素的文字說明。

請設想一個螢幕,可供使用者與好友分享目前的頁面。這個畫麵包含一個可點選的分享圖示:

可點擊的圖示列,

單憑圖示,Android 架構無法向視障使用者描述。Android 架構需要圖示的其他文字說明。

contentDescription 參數會描述視覺元素,請使用會向使用者顯示的本地化字串。

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

有些視覺元素只是單純裝飾用途,而您可能不想向使用者傳達這些元素。將 contentDescription 參數設為 null 時,您必須向 Android 架構指出這個元素沒有相關聯的動作或狀態。

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

請依此判斷特定視覺元素是否需要 contentDescription。請自問這個元素是否傳送使用者執行工作所需的資訊。如果不是,最好將該說明省略。

合併元素

無障礙服務 (例如 TalkBack 和切換控制功能) 可讓使用者在不同元素間移動畫面聚焦。元素只聚焦於適當的精細度很重要。如果畫面上的每個低階可組合項各自聚焦,使用者就必須互動許多次才能在畫面上移動。如果元素過度合併,使用者可能不知道哪些元素屬於同一群組

clickable 修飾符套用至可組合項時,Compose 會自動合併可組合項包含的所有元素。這個做法也適用於 ListItem;清單項目中的元素會合併,無障礙服務也可將其視為單一元素。

您可以將一組可組合元件組成一個邏輯群組,但該群組不可點擊,也不是清單項目的一部分。您仍希望無障礙服務將其視為單一元素。舉例來說,假設可組合項會顯示使用者的顯示圖片、使用者名稱和一些額外資訊:

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

您可以在 semantics 輔助鍵中使用 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 元素,包括使用者名稱。所有元素一併選取。

新增自訂動作

請查看下列清單項目:

一般清單項目,包含文章標題、作者和書籤圖示。

使用 TalkBack 等螢幕閱讀器聆聽螢幕上顯示的內容時,會先選取整個項目,然後選取書籤圖示。

已一併選取所有元素的清單項目。

清單項目,僅選取書籤圖示

在冗長的清單,重複性可能變得很高。更好的做法是定義自訂動作,讓使用者將項目加入書籤。請注意,您也必須明確移除書籤圖示本身的行為,以確保無障礙服務未選取書籤圖示。方法是使用 clearAndSetSemantics 修飾符完成:

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

說明元素的狀態

可組合項可以定義語意的 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() }
            )
    ) {
        /* ... */
    }
}

定義標題

應用程式有時會在同一個畫面中,使用可捲動的容器顯示大量內容。舉例來說,畫面可能會顯示使用者正在閱讀文章的完整內容:

網誌文章的螢幕截圖,且文章文字位於可捲動的容器中。

無障礙功能的使用者難以瀏覽這類畫面。為協助瀏覽,請指出哪些元素是標題。在上述範例中,每個子區段標題都可以定義為無障礙功能的標題。部分無障礙服務 (例如 Talkback) 可讓使用者從標題直接瀏覽到標題。

在 Compose 中,您可以透過定義 semantics 屬性來表示可組合項為「標題」

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

處理自訂可組合項

當您將應用程式中的特定 Material 元件替換成自訂版本時,請務必考量無障礙功能的注意事項。

假設您將 Material Checkbox 替換成自己的實作項目,您可能會忘記新增用於處理此元件無障礙屬性的 triStateToggleable 修飾符。

原則上,請查看「材質」程式庫中該元件的實作方式,並模仿能找到的任何無障礙功能。此外,請多多利用基礎修飾符 (而非 UI 層級的修飾符),因為前者內建便有考慮無障礙功能。

使用多個無障礙服務測試自訂元件實作項目,驗證其行為。

其他資源