1. 簡介
使用者可以使用硬體鍵盤與應用程式互動,通常是在平板電腦和 ChromeOS 裝置等大螢幕裝置上,但也可以在 XR 裝置上。重點是讓使用者可以透過硬體鍵盤瀏覽應用程式,並且能像使用觸控螢幕一樣順利。此外,如果您要為電視和車用螢幕設計應用程式,而這些裝置可能沒有觸控輸入功能,而是依賴 D-Pad 或旋轉編碼器,則需要套用類似的鍵盤瀏覽原則。
Compose 可讓您以統一方式處理硬體鍵盤、D-Pad 和旋轉編碼器的輸入內容。若要讓使用者在使用這些輸入方式時獲得良好體驗,關鍵原則是讓使用者能直覺且一致地將鍵盤焦點移至要互動的互動式元件。
在本程式碼實驗室中,您將瞭解以下內容:
- 如何實作常見的鍵盤焦點管理模式,以便提供直覺且一致的瀏覽體驗
- 如何測試鍵盤焦點移動行為是否如預期
必要條件
- 具備使用 Compose 建構應用程式的經驗
- 具備 Kotlin 的基本知識,包括 lambda 和協同程式
建構內容
您可以實作下列常見的鍵盤焦點管理模式:
- 鍵盤焦點移動:以 Z 字型模式從開頭到結尾、從上到下移動
- 邏輯初期焦點:將焦點設為使用者可能會互動的 UI 元素
- 焦點還原:將焦點移至使用者先前互動的 UI 元素
課程內容
- Compose 中的焦點管理基本概念
- 如何將 UI 元素設為焦點目標
- 如何要求焦點移動 UI 元素
- 如何在一組 UI 元素中將鍵盤焦點移至特定 UI 元素
軟硬體需求
- Android Studio Ladybug 以上版本
- 使用下列任一裝置執行範例應用程式:
- 配備硬體鍵盤的大螢幕裝置
- 適用於大螢幕裝置的 Android 虛擬裝置,例如可調整大小的模擬器
2. 設定
- 複製 large-screen-codelabs GitHub 存放區:
git clone https://github.com/android/large-screen-codelabs
您也可以下載並取消封存 large-screen-codelabs ZIP 檔案:
- 前往
focus-management-in-compose
資料夾: - 在 Android Studio 中開啟專案。
focus-management-in-compose
資料夾包含一個專案。 - 如果沒有 Android 平板電腦、摺疊式裝置或配備硬體鍵盤的 ChromeOS 裝置,請在 Android Studio 中開啟「裝置管理工具」,然後在「手機」類別中建立「可調整大小」裝置。
圖 1. 在 Android Studio 中設定可調整大小的模擬器。
3. 探索範例程式碼
這個專案有兩個模組:
- start:包含專案的範例程式碼。您需要修改此程式碼以完成程式碼實驗室。
- solution:包含本程式碼實驗室完成後的程式碼。
這個範例應用程式包含三個分頁:
- 焦點目標
- 焦點遍歷順序
- 焦點群組
應用程式啟動時,系統會顯示焦點目標分頁。
圖 2. 應用程式啟動時,系統會顯示「焦點目標」分頁。
ui
套件包含您要互動的下列 UI 程式碼:
App.kt
:實作分頁tab.FocusTargetTab.kt
:包含焦點目標分頁的程式碼tab.FocusTraversalOrderTab.kt
:包含焦點遍歷順序分頁的程式碼tab.FocusGroup.kt
:包含焦點群組分頁的程式碼FocusGroupTabTest.kt
-tab.FocusTargetTab.kt
的檢測設備測試 (檔案位於androidTest
資料夾中)
4. 焦點目標
焦點目標是鍵盤焦點可移動前往的目標 UI 元素。使用者可以利用 Tab
鍵或方向 (箭頭) 鍵移動鍵盤焦點:
Tab
鍵:將焦點移至下一個焦點目標或上一個焦點目標,且「僅限單一維度」。- 方向鍵:焦點可在「二維空間」中移動:上、下、左和右。
分頁是焦點目標。在範例應用程式中,當分頁獲得焦點時,分頁的背景會在視覺上更新。
圖 3. 當焦點移至焦點目標時,元件背景會變更。
根據預設,互動式 UI 元素是焦點目標
互動式元件預設為焦點目標。換句話說,如果使用者可以輕觸 UI 元素,該元素就是焦點目標。
範例應用程式在「焦點目標」分頁中顯示三張資訊卡。「第 1 張卡」和「第 3 張卡」是焦點目標,「第 2 張卡」則不是。當使用者使用 Tab
鍵將焦點從「第 1 張卡」移至「第 3 張卡」時,第 3 張卡的背景會更新。
圖 4. 應用程式焦點目標排除「第 2 張資訊卡」。
將第 2 張資訊卡修改為焦點目標
您可以將「第 2 張資訊卡」變更為互動式 UI 元素,以便將其設為焦點目標。最簡單的方法是使用 clickable
修飾符,如下所示:
- 開啟
tabs
套件中的FocusTargetTab.kt
- 使用
clickable
修飾符修改SecondCard
可組合函式,如下所示:
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
開始執行
除了第 1 張卡和第 3 張卡,使用者現在也可以將焦點移至第 2 張卡。您可以在「焦點目標」分頁中試試這項功能,確認您可以使用 Tab
鍵,將焦點從「第 1 張卡」移至「第 2 張卡」。
圖 5. 使用 Tab
鍵,將焦點從「第 1 張卡」移至「第 2 張卡」。
5. Z 型焦點遍歷
在從左至右的語言設定中,使用者預期鍵盤焦點會從左至右和由上至下移動。這種焦點遍歷順序稱為 z 型模式。
不過,Compose 在判斷 Tab
鍵的下一個焦點目標時,會忽略版面配置,並根據可組合函式呼叫的順序,採用單一維度焦點遍歷。
單一維度焦點遍歷
單一維度焦點遍歷順序取決於可組合函式呼叫的順序,而非應用程式版面配置。
在範例應用程式中,焦點會按照「焦點遍歷順序」分頁中的順序移動:
- 第 1 張資訊卡
- 第 4 張資訊卡
- 第 3 張資訊卡
- 第 2 張資訊卡
圖 6. 焦點遍歷會依照可組合函式的順序進行。
FocusTraversalOrderTab
函式會實作範例應用程式的「焦點遍歷分頁」。函式會依下列順序呼叫資訊卡的可組合函式:FirstCard
、FourthCard
、ThirdCard
和 SecondCard
。
@Composable
fun FocusTraversalOrderTab(
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(x = 256.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(y = (-151).dp)
)
}
SecondCard(
modifier = Modifier.width(240.dp)
)
}
}
以 Z 字型模式移動焦點
您可以按照下列步驟,在範例應用程式的「焦點遍歷順序」分頁中整合 Z 型焦點移動方式:
- 開啟「
tabs.FocusTraversalOrderTab.kt
」 - 從
ThirdCard
和FourthCard
可組合函式中移除偏移修飾符。 - 將分頁的版面配置從目前的兩欄一列變更為一欄兩列。
- 將
FirstCard
和SecondCard
可組合函式移至第一列。 - 將
ThirdCard
和FourthCard
可組合函式移至第二列。
修改後的程式碼如下:
@Composable
fun FocusTraversalOrderTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp),
)
SecondCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
}
開始執行
使用者現在可以以 Z 字型模式,從右到左、從上到下移動焦點。您可以在「焦點遍歷順序」分頁中按 Tab
鍵試試這項功能,確認焦點是否會以下列順序移動:
- 第 1 張資訊卡
- 第 2 張資訊卡
- 第 3 張資訊卡
- 第 4 張資訊卡
圖 7. 以 Z 字型模式進行焦點遍歷。
6. focusGroup
在「焦點群組」分頁上使用 right
方向鍵,將焦點從「第 1 張卡」移至「第 3 張卡」。由於兩張資訊卡並未並排顯示,因此這項動作可能會讓使用者感到困惑。
圖 8. 焦點從「第 1 張卡」意外移至「第 3 張卡」。
二維焦點遍歷是指版面配置資訊
按下方向鍵會觸發二維焦點遍歷。這是電視上常用的焦點遍歷方式,因為使用者會透過 D-Pad 與應用程式互動。按下鍵盤方向鍵也會觸發二維焦點遍歷,因為這類按鍵會模擬 D-Pad 的瀏覽功能。
在二維焦點遍歷作業中,系統會參照 UI 元素的幾何資訊,並判斷焦點目標以移動焦點。舉例來說,您可以使用 down
方向鍵,將焦點從焦點目標分頁移至「第 1 張資訊卡」,而按向上方向鍵則可將焦點移至焦點目標分頁。
圖 9.使用向下和向上方向鍵進行焦點遍歷。
與使用 Tab
鍵的單向焦點遍歷功能不同,二維焦點遍歷功能不會循環。舉例來說,當「第 2 張資訊卡」獲得焦點時,使用者無法透過向下鍵移動焦點。
圖 10. 焦點位於「第 2 張資訊卡」時,向下方向鍵無法移動焦點。
焦點目標位於相同層級
以下程式碼會實作上述畫面。有四個焦點目標:FirstCard
、SecondCard
、ThirdCard
和 FourthCard
。這四個焦點目標位於同一層級,ThirdCard
是版面配置中 FirstCard
右側的第一個項目。因此,使用 right
方向鍵時,焦點會從「第 1 張卡」移至「第 3 張卡」。
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
使用 focusGroup 修飾符將焦點目標分組
您可以按照下列步驟,改掉令人困惑的焦點移動方式:
- 開啟「
tabs.FocusGroup.kt
」 - 使用
focusGroup
修飾符修改FocusGroupTab
可組合函式中的Column
可組合函式。
更新後的程式碼如下:
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
focusGroup
修飾符會建立焦點群組,由已修改元件內部的焦點目標組成。焦點群組中的焦點目標和焦點群組外的焦點目標位於不同層級,且 FirstCard
可組合函式右側沒有放置焦點目標。因此,焦點不會透過 right
方向鍵從「第 1 張資訊卡」移至任何資訊卡。
開始執行
在範例應用程式的「焦點群組分頁」中,使用 right
方向鍵時,焦點不會從「第 1 張卡」移至「第 3 張卡」。
7. 要求焦點
使用者無法使用鍵盤或 D-Pad 選取要互動的任意 UI 元素。使用者必須先將鍵盤焦點移至互動式元件,才能與元素互動。
舉例來說,使用者必須先將焦點從「焦點目標分頁」移至「第 1 張資訊卡」,才能與資訊卡互動。您可以透過邏輯設定初期焦點,減少啟動使用者主要工作的動作數量。
圖 11. 按下 Tab
鍵三次,焦點會移至「第 1 張資訊卡」。
使用 FocusRequester 要求焦點
您可以使用 FocusRequester
要求焦點移動至某 UI 元素。請先將 FocusRequester
物件與 UI 元素建立關聯,再呼叫 requestFocus()
方法。
將初期焦點設為第 1 張資訊卡
您可以按照下列步驟,將初期焦點設為「第 1 張資訊卡」:
- 開啟「
tabs.FocusTarget.kt
」 - 在
FocusTargetTab
可組合函式中宣告firstCard
值,並使用remember
函式傳回的FocusRequester
物件初始化該值。 - 使用
focusRequester
修飾符修飾FirstCard
可組合函式。 - 將
firstCard
值指定為focusRequester
修飾符的引數。 - 使用
Unit
值呼叫LaunchedEffect
可組合函式,並在傳遞至LaunchedEffect
可組合函式的 lambda 中,針對firstCard
值呼叫 requestFocus() 方法。
在第二步和第三步驟中,系統會建立 FocusRequester
物件,並將其與 UI 元素建立關聯。在第五個步驟中,當 FocusdTargetTab
可組合函式首次組合時,系統會要求將焦點移至相關聯的 UI 元素。
更新後的程式碼如下所示:
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val firstCard = remember { FocusRequester() }
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.focusRequester(focusRequester = firstCard)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
LaunchedEffect(Unit) {
firstCard.requestFocus()
}
}
開始執行
現在,選取分頁時,鍵盤焦點會移至「焦點目標」分頁標籤中的「第 1 張資訊卡」。您可以試著切換分頁。此外,應用程式啟動時會選取「第 1 張卡」。
圖 12. 選取「焦點目標」分頁後,焦點會移至「第 1 張資訊卡」。
8. 將焦點移至所選分頁
您可以指定鍵盤焦點進入焦點群組時的焦點目標。舉例來說,當使用者將焦點移至分頁列時,您可以將焦點移至所選分頁。
您可以按照下列步驟實作這項行為:
- 開啟
App.kt
。 - 在
App
可組合函式中宣告focusRequesters
值。 - 使用
remember
函式傳回的FocusRequester
物件清單傳回值,來初始化focusRequesters
值。傳回清單的長度應與Screens.entries
的長度相同。 - 使用
focusRequester
修飾符修改「分頁」可組合函式,將focusRequester
值的每個FocusRequester
物件與Tab
可組合函式建立關聯。 - 使用
focusProperties
修飾符和focusGroup
修飾符修改 PrimaryTabRow 可組合函式。 - 將 lambda 傳遞至
focusProperties
修飾符,並將enter
屬性與另一個 lambda 建立關聯。 - 從與
enter
屬性相關聯的 lambda 中,傳回以focusRequesters
值中的selectedTabIndex
值建立索引的 FocusRequester。
修改後的程式碼如下所示:
@Composable
fun App(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var selectedScreen by rememberSaveable { mutableStateOf(Screen.FocusTarget) }
val selectedTabIndex = Screen.entries.indexOf(selectedScreen)
val focusRequesters = remember {
List(Screen.entries.size) { FocusRequester() }
}
Column(modifier = modifier) {
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier
.focusProperties {
enter = {
focusRequesters[selectedTabIndex]
}
}
.focusGroup()
) {
Screen.entries.forEachIndexed { index, screen ->
Tab(
selected = screen == selectedScreen,
onClick = { selectedScreen = screen },
text = { Text(stringResource(screen.title)) },
modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
)
}
}
when (selectedScreen) {
Screen.FocusTarget -> {
FocusTargetTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp),
)
}
Screen.FocusTraversalOrder -> {
FocusTraversalOrderTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
Screen.FocusRestoration -> {
FocusGroupTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
}
}
}
您可以使用 focusProperties
修飾符控制焦點移動方式。在傳遞至修飾符的 lambda 中,修改 FocusProperties。當使用者在已修改的 UI 元素獲得焦點時按下 Tab
鍵或方向鍵,系統會參照 FocusProperties 選擇焦點目標。
設定 enter
屬性時,系統會評估屬性所設的 lambda,並移至與評估 lambda 傳回的 FocusRequester
物件相關聯的 UI 元素。
開始執行
現在,當使用者將焦點移至分頁列時,鍵盤焦點會移至所選分頁。您可以按照下列步驟嘗試這項做法:
- 執行應用程式
- 選取「焦點群組」分頁標籤
- 使用
down
方向鍵,將焦點移至「第 1 張資訊卡」。 - 使用
up
方向鍵移動焦點。
圖 13. 焦點會移至所選分頁。
9. 焦點還原
使用者希望在工作中斷時,能輕鬆恢復工作。焦點還原功能可讓您在中斷後恢復工作。焦點還原會將鍵盤焦點移至先前選取的 UI 元素。
焦點還原功能的典型用途是影片串流應用程式的主畫面。畫面上會顯示多個影片內容清單,例如某個類別的電影或電視節目的集數。使用者會瀏覽各個清單,尋找有趣的內容。有時,使用者會返回先前查看的清單,繼續瀏覽。有了焦點還原功能,使用者就能繼續瀏覽,而無須將鍵盤焦點移至清單中上次查看的項目。
focusRestorer 修飾符會將焦點還原至焦點群組
使用 focusRestorer
修飾符,可將焦點儲存及還原至焦點群組。焦點離開焦點群組時,焦點會儲存先前焦點項目的參照。然後,當焦點重新進入焦點群組時,焦點會還原為先前聚焦的項目。
將焦點還原功能整合到「焦點群組」分頁
範例應用程式的「焦點群組」分頁有一個資料列包含「第 2 張卡」、「第 3 張卡」和「第 4 張卡」。
圖 14. 焦點群組包含「第 2 張卡」、「第 3 張卡」和「第 4 張卡」。
您可以按照下列步驟,在資料列中整合焦點還原功能:
- 開啟「
tab.FocusGroupTab.kt
」 - 使用
focusRestorer
修飾符修改FocusGroupTab
可組合函式中的Row
可組合函式。應先呼叫修飾符,再呼叫focusGroup
修飾符。
修改後的程式碼如下所示:
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.focusRestorer()
.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
開始執行
這時「焦點群組」分頁中的資料列會還原焦點,您可以按照下列步驟進行測試:
- 選取「焦點群組」分頁標籤
- 將焦點移至第 1 張資訊卡
- 使用
Tab
鍵將焦點移至「第 4 張資訊卡」 - 使用
up
方向鍵將焦點移至「第 1 張資訊卡」 - 按下
Tab
鍵
鍵盤焦點會移至「第 4 張資訊卡」,因為 focusRestorer
修飾符會儲存資訊卡的參照,並在鍵盤焦點進入設為資料列的焦點群組時還原焦點。
圖 15. 按下向上方向鍵後,焦點會返回「第 4 張資訊卡」,接著按下 Tab
鍵。
10. 編寫測試
您可以使用測試來測試已實作的鍵盤焦點管理功能。Compose 提供的 API 可用於測試 UI 元素是否獲得焦點,並在 UI 元件上執行按鍵操作。詳情請參閱「在 Jetpack Compose 中測試」程式碼實驗室。
測試「焦點目標」分頁
您在上一節修改了 FocusTargetTab
可組合函式,將「第 2 張資訊卡」設為焦點目標。針對您在上一節手動執行的實作項目編寫測試。您可以按照下列步驟編寫測試:
- 開啟
FocusTargetTabTest.kt
。您將在後續步驟中修改testSecondCardIsFocusTarget
函式。 - 針對「第 1 張資訊卡」,在
SemanticsNodeInteraction
物件上呼叫requestFocus
方法,要求焦點移至「第 1 張資訊卡」。 - 確認「第 1 張資訊卡」已使用
assertIsFocused()
方法獲得焦點。 - 呼叫
pressKey
方法,並在 lambda 中傳遞Key.Tab
值至performKeyInput
方法,即可執行Tab
按鍵動作。 - 針對「第 2 張資訊卡」,在
SemanticsNodeInteraction
物件上呼叫assertIsFocused()
方法,測試鍵盤焦點是否會移至「第 2 張資訊卡」。
更新後的程式碼如下所示:
@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
composeTestRule.setContent {
LocalInputModeManager
.current
.requestInputMode(InputMode.Keyboard)
FocusTargetTab(onClick = {})
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Ensure the 1st card is focused
composeTestRule
.onNodeWithText(context.getString(R.string.first_card))
.requestFocus()
.performKeyInput { pressKey(Key.Tab) }
// Test if focus moves to the 2nd card from the 1st card with Tab key
composeTestRule
.onNodeWithText(context.getString(R.string.second_card))
.assertIsFocused()
}
開始執行
您可以按一下 FocusTargetTest
類別宣告左側的三角形圖示,執行測試。詳情請參閱「在 Android Studio 中測試」一文的「執行測試」一節。
11. 恭喜
非常好! 您已瞭解鍵盤焦點管理的構成元素:
- 焦點目標
- 焦點遍歷
您可以使用下列 Compose 修飾符控制焦點遍歷順序:
focusGroup
修飾符focusProperties
修飾符
您已實作使用硬體鍵盤的典型使用者體驗模式、初期焦點和焦點還原功能。這些模式是透過結合下列 API 來實作:
FocusRequester
類別focusRequester
修飾符focusRestorer
修飾符LaunchedEffect
可組合函式
您可以使用檢測設備測試來測試已實作的使用者體驗。Compose 提供執行按鍵操作的方法,並測試 SemanticsNode
是否有鍵盤焦點。