1. 簡介
在本程式碼研究室中,您將瞭解如何使用 Jetpack Compose 強化應用程式的無障礙功能。我們會逐步說明幾種常見用途,並逐步改善範例應用程式。包括觸控目標大小、內容說明、點選標籤等等。
視障、色覺障礙、聽障、精細動作障礙、認知障礙和其他許多身心障礙人士會使用 Android 裝置,處理各種日常事務。開發應用程式時將無障礙設計納入考量,能帶來更優異的使用者體驗,尤其可以造福上述族群,或有其他無障礙需求的使用者。
在本程式碼研究室中,我們將使用 TalkBack 來手動測試程式碼變更。TalkBack 是一項無障礙服務,主要供視障人士使用。此外,請務必使用其他無障礙服務測試任何程式碼變更內容,例如「切換控制功能」。
在 Jetnews 應用程式中使用 TalkBack。
課程內容
在本程式碼研究室,您將學到:
- 如何透過增加觸控目標大小,滿足精細動作障礙人士的需求。
- 什麼是語意屬性,以及其變更方式。
- 如何提供更容易存取的可組合項資訊。
軟硬體需求
- 有 Kotlin 語法 (包括 lambda) 的經驗。
- 有 Compose 的基本經驗。建議您先學習 Jetpack Compose 的基本程式碼研究室,再學習本程式碼研究室
- 已啟用 TalkBack 的 Android 裝置或模擬器。
建構項目
在本程式碼研究室中,我們將改善新聞閱讀應用程式的無障礙功能。首先,我們將從不具備重要無障礙功能的應用程式著手,並運用習得的知識,提高應用程式的實用性,進一步滿足具有無障礙需求的使用者。
2. 開始設定
在這個步驟中,您將下載程式碼,其中包含一個簡易的新聞閱讀器應用程式。
軟硬體需求
取得程式碼
您可以在 codelab-android-compose GitHub 存放區中找到本程式碼實驗室的程式碼。如要複製該存放區,請執行下列命令:
$ git clone https://github.com/android/codelab-android-compose
或者,您也可以下載兩個 ZIP 檔案:
查看範例應用程式
您剛才下載的程式碼包含所有 Compose 程式碼研究室可用的程式碼。如要完成本程式碼研究室,請在 Android Studio 中開啟 AccessibilityCodelab
專案。
建議您先從 main
分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。
設定 TalkBack
在本程式碼研究室中,我們會使用 TalkBack 檢查變更。在使用實體裝置進行測試時,請遵循以下操作說明開啟 TalkBack。根據預設,模擬器並未安裝 TalkBack。選擇包含 Play 商店的模擬器,並下載 Android 無障礙套件。
3. 觸控目標大小
任何使用者可點擊、輕觸或進行互動的螢幕元素,都必須設為適當大小,方便使用者進行互動。您應確認這些元素的寬度和高度至少有 48dp。
如果這些控制項會動態調整大小,或是根據內容大小進行調整,建議您採用 sizeIn
修飾符設定這些項目的尺寸下限。
有些 Material 元件可為您設定這些大小。舉例來說,按鈕可組合函式的 MinHeight
會設為 36 dp,並使用 8 dp 垂直邊框間距。這會增加至必要的 48dp 高度。
開啟範例應用程式並執行 TalkBack 時,您會發現貼文資訊卡中交叉圖示的觸控目標太小。我們希望讓此觸控目標的大小至少達到 48dp。
在以下螢幕截圖中,左側為原始應用程式,右側為經過改善的解決方案。
以下將說明實作方式,並檢查此可組合項的大小。開啟 PostCards.kt
並尋找 PostCardHistory
可組合項。如您所見,實作時會將溢位選單圖示的大小設為 24dp:
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
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(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
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(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
透過 TalkBack 導覽畫面時,現可正確顯示 48 dp 的觸控目標區域。此外,IconButton
還會加入漣漪效果指標,讓使用者知道此元素可以點選。
4. 按一下標籤
根據預設,應用程式中可供點選的元素不會說明點選該元素後會執行何種操作。因此,TalkBack 等無障礙服務會使用非常籠統的預設說明。
為了向具有無障礙需求的使用者提供最佳體驗,我們可以提供具體說明,解釋使用者點選此元素時會有什麼影響。
在 Jetnews 應用程式中,使用者點選各種貼文資訊卡後,即可閱讀完整貼文。根據預設,系統會讀出可點選元素的內容,以及「Double tap to activate」這段文字。我們希望改為提供更具體的說明,並採用「Double tap to read article」(輕觸兩下即可閱讀文章) 文字。相較於理想解決方案,原始版本看起來如下所示:
變更可組合項的點擊標籤。之前 (左側) 與之後 (右側) 的對比。
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 現在可正確朗讀「Double tap to read article」(輕觸兩下即可閱讀文章)。
主畫面上的其他貼文資訊卡也有相同的一般點擊標籤。接著來看看 PostCardPopular
可組合函式的實作情況,並更新其點擊標籤:
@Composable
fun PostCardPopular(
// ...
) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier.size(280.dp, 240.dp),
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
此可組合函式會在內部使用 Card
可組合函式,使您無法直接設定其點擊標籤。您可以改用 semantics
修飾符設定點擊標籤:
@Composable
fun PostCardPopular(
post: Post,
navigateToArticle: (String) -> Unit,
modifier: Modifier = Modifier
) {
val readArticleLabel = stringResource(id = R.string.action_read_article)
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier
.size(280.dp, 240.dp)
.semantics { onClick(label = readArticleLabel, action = null) },
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
5. 自訂操作
許多應用程式都會顯示某種清單,而每個清單項目各包含一或多個動作。使用螢幕閱讀器時,系統會一再重複聚焦於相同動作,因此這類清單的導覽作業可能會變得相當繁瑣。
我們可以將自訂無障礙動作新增至可組合項。這樣一來,與同個清單項目相關的動作就會分到同一群組中。
Jetnews 應用程式會顯示使用者可閱讀的文章清單。每個清單項目皆包含一個動作,表示使用者希望減少看到這個主題的內容。在此區段中,我們會將此動作移至自訂無障礙動作,讓您更輕鬆地瀏覽清單。
左側顯示的是預設情況,每個交叉圖示皆可聚焦。右側則顯示解決方案,讓動作包含在 TalkBack 的自訂動作中:
為貼文項目新增自訂動作。之前 (左側) 與之後 (右側)。
接著開啟 PostCards.kt
,瞭解 PostCardHistory
可組合項的實作方式。請注意,Row
和 IconButton
的可點選屬性皆使用 Modifier.clickable
和 onClick
:
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
Modifier.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
根據預設,Row
和 IconButton
可組合函式皆可供點選,因此 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(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
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(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(
modifier = Modifier.clearAndSetSemantics { },
onClick = { openDialog = true }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = showFewerLabel
)
}
}
}
// ...
}
我們現可在 TalkBack 中,使用自訂動作彈出式視窗來套用動作。隨著清單項目中的動作數量增加,這也會更加相關。
6. 視覺元素說明
並非每個應用程式使用者都能查看或解讀應用程式顯示的視覺元素,例如圖示和插圖。無障礙服務也不能基於個別像素而呈現視覺元素。因此身為開發人員,您必須在應用程式中將關於視覺元素的更多資訊傳遞至無障礙服務。
Image
和 Icon
等視覺可組合項包含參數 contentDescription
。您會傳遞該視覺元素的本地化說明;如果元素只有裝飾功能,則會傳遞 null
。
應用程式中的文章畫面缺少部分內容說明。執行應用程式並選取熱門文章,以前往文章畫面。
新增視覺內容說明。之前 (左側) 與之後 (右側)。
若未提供任何資訊,點選左上方的導覽圖示時,只會宣告「Button, double tap to activate」。這並不會向使用者告知啟用該按鈕後會採取哪些動作。開啟 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. 標題
若是包含大量文字的畫面,例如文章畫面,有視覺障礙的使用者會難以迅速找到所需的章節內容。為了方便辨識,我們可以指明文字的哪些部分為標題。使用者只要向上或向下滑動,就能快速瀏覽不同的標題。
根據預設,系統無法將任何可組合項標示為標題,因此無法執行瀏覽動作。我們希望讓文章畫面依標題瀏覽來提供標題:
新增標題。之前 (左側) 與之後 (右側)。
本文中的標題定義於 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
屬性,將這些可組合項合併在一起。
接著來看看文章畫面。大多數元素皆會取得正確的焦點層級。但就目前而言,文章的中繼資料會朗讀為數個獨立的項目。只要將其合併為單一可聚焦實體,即可改善此情況:
合併可組合項。之前 (左側) 與之後 (右側)。
開啟 PostContent.kt
並檢查 PostMetadata
可組合項:
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
我們可以指示頂層列合併子系,進而產生想要的行為:
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row(Modifier.semantics(mergeDescendants = true) {}) {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
9. 切換按鈕與核取方塊
當 TalkBack 選取 Switch
和 Checkbox
等可切換元素時,這些元素會大聲讀出自己的已勾選狀態。然而若無背景資訊,或許會難以理解這些可切換元素的意義。我們可以提升可切換元素的狀態,納入可切換元素的背景資訊,讓使用者只要按下可組合函式本身或其說明標籤,即可切換 Switch
或 Checkbox
。
我們可在「Interests」(興趣) 畫面中查看此範例。在主畫面中開啟導覽匣,即可在其中瀏覽。「Interests」(興趣) 畫面會顯示使用者可訂閱的主題清單。根據預設,系統會分別聚焦畫面上的核取方塊和標籤,因此使用者不容易瞭解相關背景資訊。我們希望將整個 Row
設為可切換:
使用核取方塊。之前 (左側) 與之後 (右側)。
開啟 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」(未勾選)。我們可以將此說明替換為專屬自訂說明:
新增狀態說明。之前 (左側) 與之後 (右側)。
我們可以繼續處理在上個步驟中調整的 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。
說明文件
如需有關這些主題的更多資訊和指南,請參閱以下說明文件: