Compose 中的無障礙功能

Compose 中撰寫的應用程式應支援不同使用者的無障礙需求。無障礙服務可用來將畫面上顯示的內容,轉換成特定需求使用者所需的適當格式。為了支援無障礙服務,應用程式會透過 Android 架構中的 API 公開有關 UI 元素的語意資訊。之後,Android 架構會將該語意資訊告知無障礙服務。每項無障礙服務均可選擇向使用者說明應用程式的最佳方式。Android 提供多種無障礙服務,包括 TalkbackSwitch Access

語意

Compose 使用語意屬性將資訊傳遞給無障礙服務。語意屬性會提供向使用者顯示的 UI 元素相關資訊。大部分的內建元件,例如 Text 和 Button,會將這些語意屬性填入從可組合項和子項所推測出的資訊。toggleableclickable 等部分輔助鍵也會設定特定語意屬性。不過,有時架構需要更多資訊,才能向使用者說明 UI 元素。

本文件說明多種情境中,您需要在可組合項中明確加入其他資訊,方可向 Android 架構正確說明。同時也會說明如何針對特定的可組合項完全取代語意資訊。本文假設您對 Android 中的無障礙功能有基本的瞭解。

常見用途

如要協助有無障礙需求者成功使用您的應用程式,建議您按照本頁所述的最佳做法操作。

考量最低觸控目標大小

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

「材質」元件,例如 CheckboxRadioButtonSwitchSlider以及Surface:請在內部設定此大小下限,但僅限元件可以接收使用者動作時。舉例來說,如果 CheckboxonCheckedChange 參數設為非空值,系統就會在其中包含至少 48dp 的寬度和高度。

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

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

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

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

@Composable
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
fun DefaultPreview() {
   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
fun DefaultPreview() {
   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
fun ArticleListItem(openArticle: () -> Unit) {
   Row(
       Modifier.clickable(
           // R.string.action_read_article = "read article"
           onClickLabel = stringResource(R.string.action_read_article),
           onClick = openArticle
       )
   ) {
       // ..
   }
}

如果您無法存取可點擊的修飾符,則可在語意修飾符內設定點擊標籤:

@Composable
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)
       }
   ) {
       // ..
   }
}

描述視覺元素

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

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

可點擊的圖示列,其中醒目顯示「共用」圖示

單憑藉圖示,Android 架構無從得知該如何向視障使用者說明。Android 架構需要圖示的其他文字說明。

contentDescription 參數是用來描述視覺元素。您應使用本地化字串,因為系統會將這個字串提供給使用者。

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

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

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

  Image(
    bitmap = 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
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 中,您可以透過定義語意屬性的方式,指出可組合項為標題

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

建立自訂低階可組合項

更進階的用途是將應用程式中的特定「材質」元件替換成自訂版本。在這種情況下,請務必考量無障礙功能。假設您將「材質」Checkbox 替換成自己的實作項目,使用者很容易忘記加入負責處理此元件無障礙屬性的 triStateToggleable 修飾符。

原則上,您應在「材質」程式庫中查看該元件的實作方式,並模仿能找到的任何無障礙功能。 此外,請多多利用基礎修飾符 (而非 UI 層級的修飾符),因為前者內建便有考慮無障礙功能。請務必透過多項無障礙服務測試自訂元件的實作,驗證其行為。

瞭解詳情

如要進一步瞭解在 Compose 程式碼中支援無障礙功能,請參閱 Jetpack Compose 程式碼研究室的無障礙功能