Jetpack Compose 中的無障礙功能

1. 簡介

在本程式碼研究室中,您將瞭解如何使用 Jetpack Compose 改善應用程式的無障礙功能。我們會逐步說明幾種常見用途,並逐步改善範例應用程式。包括觸控目標大小、內容說明、點選標籤等等。

身心障礙人士 (包括視障、色盲、聽障、精細動作障礙、認知障礙和許多其他身心障礙狀況的使用者) 會使用 Android 裝置來處理日常事務。如果您在開發應用程式時將無障礙設計納入考量,就能帶來更優異的使用者體驗,尤其能造福上述和其他無障礙需求的使用者。

在本程式碼研究室中,我們將使用 TalkBack 來手動測試程式碼變更。TalkBack 是一項無障礙服務,主要供視障人士使用。此外請務必測試其他無障礙服務的程式碼變更,例如「切換控制功能」。

TalkBack 焦點方框會在 Jetnews 的主畫面中移動。在畫面底部會顯示 TalkBack 公告文字。

在 Jetnews 應用程式中使用 TalkBack。

課程內容

在本程式碼研究室,您將學到:

  • 如何透過增加觸控目標大小,滿足精細動作障礙人士的需求。
  • 什麼是語意屬性,以及其變更方式。
  • 如何提供更容易存取的可組合項資訊。

軟硬體需求

建構項目

在本程式碼研究室中,我們將改善 Google 新聞閱讀應用程式的無障礙設計。首先,我們將從不具備重要無障礙功能的應用程式開始著手,並套用習得的知識,讓具有無障礙功能需求的使用者更能方便使用。

2. 開始設定

在此步驟中,您將下載這個程式碼,其中包含一個簡易的新聞閱讀器應用程式。

您需要準備的項目

取得程式碼

您可以在 android-compose-codelabs GitHub 存放區中找到本程式碼研究室的程式碼。如要複製該存放區,請執行下列命令:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

或者,您也可以下載兩個 ZIP 檔案:

查看範例應用程式

您剛才下載的程式碼包含所有 Compose 程式碼研究室可用的程式碼。如要完成本程式碼研究室,請在 Android Studio 中開啟 AccessibilityCodelab 專案。

建議您先從 main 分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。

設定 TalkBack

在本程式碼研究室中,我們會使用 TalkBack 檢查變更。在使用實體裝置進行測試時,請遵循以下操作說明開啟 TalkBack。根據預設,模擬器並未安裝 TalkBack。選擇包含 Play 商店的模擬器,並下載 Android 無障礙套件

3. 觸控目標大小

任何使用者可點擊、輕觸或進行互動的螢幕元素,都必須設為適當大小,方便使用者進行互動。您應確認這些元素的寬度和高度至少有 48dp

如果這些控制項採用動態調整大小,或是會根據其內容調整大小,建議您採用 sizeIn 修飾符來設定這些項目的尺寸下限。

有些 Material 元件可為您設定這些大小。舉例來說,「按鈕」可組合項的 MinHeight 會設為 36dp,並使用 8dp 垂直邊框間距。這會增加至必要的 48dp 高度。

開啟範例應用程式並執行 TalkBack 時,您會發現貼文資訊卡中的交叉圖示觸控目標極小。我們希望讓此觸控目標的大小至少達到 48dp。

在以下的螢幕截圖中,左側為原始應用程式,右側為經過改善的解決方案。

清單項目比較結果顯示左側為小型交叉圖示外框,右側為大型外框。

以下將說明實作方式,並檢查此可組合項的大小。開啟 PostCards.kt 並尋找 PostCardHistory 可組合項。如您所見,實作時會將溢位選單圖示的大小設為 24dp:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...

   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .size(24.dp)
           )
       }
   }
   // ...
}

如要增加此 Icon 的觸控目標大小,您可以新增邊框間距:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .padding(12.dp)
                   .size(24.dp)
           )
       }
   }
   // ...
}

在我們的使用案例中,您將更輕鬆地確認觸控目標至少為 48dp。我們可以使用 Material 元件 IconButton 來處理以下程式碼:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

透過 TalkBack 瀏覽螢幕畫面時,現已可正確顯示 48dp 的觸控目標區域。此外,IconButton 還會加入漣漪效果指標,以讓使用者知道元素可供點選。

4. 按一下標籤

根據預設,應用程式中可供點選的元素不會提供有關該元素將執行何種點擊的資訊。因此,TalkBack 等無障礙服務會使用較為一般的預設說明。

為了向具有無障礙需求的使用者提供最佳體驗,我們可以提供具體說明,以解釋當使用者按一下此元素時會有什麼影響。

在 Jetnews 應用程式中,使用者可以點選各種貼文資訊卡來閱讀完整貼文。根據預設,系統會讀出可點選元素的內容,後面再加上「輕觸兩下即可啟用」文字。我們會改為提供更明確的功能,並使用「輕觸兩下即可閱讀文章」文字。相較於理想解決方案,原始版本看起來如下所示:

已啟用 TalkBack 的兩個螢幕錄製畫面,輕觸直向清單中的貼文,以及橫向輪轉介面中的貼文。

變更可組合項的點擊標籤。之前 (左側) 與之後 (右側)。

SurfaceCard 等可組合項以及 clickable 修飾符皆包含參數,我們可透過該參數直接設定此點擊標籤。

接著將說明關於 PostCardHistory 實作的方式:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

如您所見,此實作使用 clickable 修飾符。如要設定點擊標籤,可設定 onClickLabel 參數:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable(
               // R.string.action_read_article = "read article"
               onClickLabel = stringResource(R.string.action_read_article)
           ) {
               navigateToArticle(post.id)
           }
   ) {
       // ...
   }
}

TalkBack 現在可正確朗讀「輕觸兩下即可閱讀文章」

主畫面上的其他貼文資訊卡也有相同的一般點擊標籤。接著來看看 PostCardPopular 可組合項的實作情況,並更新其點擊標籤:

@Composable
fun PostCardPopular(
   // ...
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

此可組合項會在內部使用 Card 可組合項,其中的超載可讓您傳遞點擊標籤:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PostCardPopular(
   post: Post,
   navigateToArticle: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) },
       onClickLabel = stringResource(id = R.string.action_read_article)
   ) {
       // ...
   }
}

5. 自訂操作

許多應用程式都會顯示某種清單,而清單中的各個項目皆包含一或多個動作。使用螢幕閱讀器時,由於使用者可能會一直重複同樣的動作,因此在瀏覽此類清單時相當麻煩。

我們可以將自訂無障礙動作新增至可組合項。這樣一來,與同個清單項目相關的動作就會分到同一群組中。

Jetnews 應用程式會顯示使用者可閱讀的文章清單。每個清單項目皆包含一個動作,表示使用者希望減少看到這個主題的內容。在此區段中,我們會將此動作移至自訂無障礙動作,讓您更輕鬆地瀏覽清單。

您可以在左側查看預設情況,其中的每個交叉圖示皆可聚焦。畫面右側會顯示解決方案,而該動作會包含在 TalkBack 的自訂動作中:

已啟用 TalkBack 的兩個螢幕錄製畫面。左側的畫面會顯示如何讓貼文項目上的交叉圖示可供選取。輕觸兩下以開啟對話方塊。畫面右側會顯示輕觸三下手勢,可開啟自訂「動作」選單。只要輕觸「Show fewer of this」(顯示較少內容) 動作,即會開啟相同的對話方塊。

為貼文項目新增自訂動作。之前 (左側) 與之後 (右側)。

接著開啟 PostCards.kt,瞭解 PostCardHistory 可組合項的實作方式。請注意,RowIconButton 的可點選屬性皆使用 Modifier.clickableonClick

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

根據預設,RowIconButton 可組合項皆為可點選,因此 TalkBack 會聚焦於這些可組合項。清單中的每個項目皆會發生此情況,亦即在瀏覽清單時需要頻繁滑動。您希望將與 IconButton 相關的動作,納入清單項目中做為自訂動作。您可以使用 clearAndSetSemantics 修飾符,指示「無障礙服務」不要與此 Icon 互動:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

不過,移除 IconButton 的語意後,就無法再執行動作。我們可以將動作新增至清單項目,只要在 semantics 修飾符中新增自訂動作即可:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   val showFewerLabel = stringResource(R.string.cd_show_fewer)
   Row(
        Modifier
            .clickable(
                onClickLabel = stringResource(R.string.action_read_article)
            ) {
                navigateToArticle(post.id)
            }
            .semantics {
                customActions = listOf(
                    CustomAccessibilityAction(
                        label = showFewerLabel,
                        // action returns boolean to indicate success
                        action = { openDialog = true; true }
                    )
                )
            }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

我們現可在 TalkBack 中,使用自訂動作彈出式視窗來套用動作。隨著清單項目中的動作數量增加,這也會更加相關。

6. 視覺元素說明

不是每個應用程式使用者都能查看或解讀應用程式中顯示的視覺元素,例如圖示和插圖。無障礙服務也不能基於個別像素而呈現視覺元素。因此身為開發人員,您必須在應用程式中將關於視覺元素的更多資訊傳遞至無障礙服務。

ImageIcon 等視覺可組合項包含參數 contentDescription。您會在此處傳遞該視覺元素的「本地化」說明,若純粹為裝飾性元素,則為 null

應用程式中的文章畫面缺少部分內容說明。執行應用程式並選取熱門文章,以前往文章畫面。

已啟用 TalkBack 的兩個螢幕錄製畫面,輕觸文章畫面中的返回按鈕。左側為呼叫「按鈕:輕觸兩下以啟用」。右側為呼叫「向上瀏覽:輕觸兩下以啟用」。

新增視覺內容說明。之前 (左側) 與之後 (右側)。

若未提供任何資訊,左上方的導覽圖示只會宣告「按鈕,輕觸兩下即可啟用」。這並不會向使用者告知啟用該按鈕後會採取哪些動作。開啟 ArticleScreen.kt

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = null
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

在圖示中加入有意義的內容說明:

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = stringResource(
                               R.string.cd_navigate_up
                           )
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

本文中的另一個視覺元素為標題圖片。在這個案例中,此圖片僅為裝飾性,並未顯示任何需要傳達給使用者的資訊。因此,系統會將內容說明設為 null,並在使用無障礙服務時略過該元素。

畫面中的最後一個視覺元素是個人資料相片。由於我們在此案例中使用的是一般顯示圖片,因此無須在此處新增內容說明。當我們使用此作者的實際個人資料相片時,可要求對方提供適當的內容說明

7. 標題

若畫面包含大量文字 (例如文章畫面),有視覺障礙的使用者會非常難以迅速找到想要的部分。為了方便辨識,我們可以指明文字的哪些部分為標題。使用者只要向上或向下滑動,就能快速瀏覽不同的標題。

根據預設,系統無法將任何可組合項標示為標題,因此無法執行瀏覽動作。我們希望讓文章畫面依標題瀏覽來提供標題:

已啟用 TalkBack 的兩個螢幕錄製畫面,向下滑動以瀏覽標題。左側畫面會讀出「沒有下一個標題」。右側畫面會循環顯示標題,並大聲讀出每個標題。

新增標題。之前 (左側) 與之後 (右側)。

本文中的標題定義於 PostContent.kt。讓我們開啟該檔案,並捲動至 Paragraph 可組合項:

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp),
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

此處的 Header 定義為簡易 Text 可組合項。我們可設定 heading 語意屬性,以指明此可組合項為標題。

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp)
                     .semantics { heading() },
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

8. 自訂合併

如前述步驟所示,TalkBack 等無障礙服務會依元素瀏覽畫面元素。根據預設,在 Jetpack Compose 中,每個低層級可組合項會設定至少一個語意屬性來接收焦點。舉例來說,Text 可組合項設定 text 語意屬性,因此會接收焦點。

不過,若在螢幕畫面上顯示過多可聚焦元素,可能會導致使用者在逐一瀏覽時造成混淆。您可使用 semantics 修飾符與其 mergeDescendants 屬性,將這些可組合項合併在一起。

接著來看看文章畫面。大多數元素皆會取得正確的焦點層級。但就目前而言,文章的中繼資料會朗讀為數個獨立的項目。只要將其合併為單一可聚焦實體,即可改善此情況:

已啟用 TalkBack 的兩個螢幕錄製畫面。左側畫面會針對「Author」(作者) 與「Metadata」(中繼資料) 欄位,顯示獨立的綠色 TalkBack 方框。右側畫面會在兩個欄位周圍顯示單一方框,並讀取串連內容。

合併可組合項。之前 (左側) 與之後 (右側)。

開啟 PostContent.kt 並檢查 PostMetadata 可組合項:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

我們可以指示頂層資料列合併其子系,進而產生想要的行為:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row(Modifier.semantics(mergeDescendants = true) {}) {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

9. 切換按鈕與核取方塊

當 TalkBack 選取諸如 SwitchCheckbox 等可切換元素時,其會大聲讀出已勾選狀態。然而若無背景資訊,或許會難以理解這些可切換元素的意義。我們可以將可切換狀態往上移,藉此加入可切換元素的背景資訊,讓使用者只要按下可組合項本身或其說明標籤,即可切換 SwitchCheckbox

我們可在「Interests」(興趣) 畫面中查看此範例。在主畫面中開啟導覽匣,即可在其中瀏覽。「Interests」(興趣) 畫面會顯示使用者可訂閱的主題清單。根據預設,此畫面中的核取方塊會與其標籤分隔聚焦,因此難以瞭解其背景資訊。我們希望將整個 Row 設為可切換:

已啟用 TalkBack 的兩個螢幕錄製畫面,顯示具有可選取主題清單的興趣畫面。在左側畫面中,TalkBack 會個別勾選每個核取方塊。在右側畫面中,TalkBack 會選取整列。

使用核取方塊。之前 (左側) 與之後 (右側)。

開啟 InterestsScreen.kt,瞭解 TopicItem 可組合項的實作方式:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = { onToggle() },
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

如您所見,Checkbox 具有用於切換元素的 onCheckedChange 回呼。我們可以將此回呼升至整個 Row 的層級:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

10. 狀態說明

在上一步驟中,我們已將切換行為從 Checkbox 升至父項 Row。我們可新增可組合項狀態的自訂說明,進一步改善此元素的無障礙功能。

根據預設,Checkbox 狀態會讀取為「Ticked」(已勾選) 或「Not tick」(未勾選)。我們可以將此說明替換為專屬自訂說明:

已啟用 TalkBack 的兩個螢幕錄製畫面,在興趣畫面中輕觸主題。左側畫面宣告「Not ticked」(未勾選),右側畫面則宣告「Not subscribed」(未訂閱)。

新增狀態說明。之前 (左側) 與之後 (右側)。

我們可以繼續處理在上個步驟中調整的 TopicItem 可組合項。

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

您可使用 semantics 修飾符中的 stateDescription 屬性來新增自訂狀態說明:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
   val stateSubscribed = stringResource(R.string.state_subscribed)
   Row(
       modifier = Modifier
           .semantics {
               stateDescription = if (selected) {
                   stateSubscribed
               } else {
                   stateNotSubscribed
               }
           }
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

11. 恭喜!

恭喜!您已成功完成本程式碼研究室,並進一步瞭解 Compose 中的無障礙功能。您已瞭解觸控目標、視覺元素說明和狀態說明。您已新增點擊標籤、標題和自訂動作。您已瞭解如何新增自訂合併功能,以及如何使用切換按鈕和核取方塊。將這些學習成果運用在您的應用程式中,可大幅改善應用程式的無障礙設計!

請參閱 Compose 課程中的其他程式碼研究室。另外亦可參閱其他程式碼範例,包括 Jetnews。

說明文件

如需有關這些主題的更多資訊和指南,請參閱以下說明文件: