使用 Jetpack Compose 新增鍵盤、滑鼠、觸控板和觸控筆支援功能

1. 簡介

如果應用程式支援標準手機,就適用於大螢幕裝置,例如平板電腦、摺疊式裝置和 ChromeOS 裝置。

使用者會期待應用程式在大螢幕上提供與小螢幕上同等或更佳的使用者體驗。

在大螢幕裝置上,使用者也更有可能搭配實體鍵盤和指標裝置 (例如滑鼠或觸控板) 使用應用程式。Chromebook 等某些大螢幕裝置提供實體鍵盤和指標裝置,其他裝置則會連接 USB 或藍牙鍵盤和指標裝置。使用者期望搭配實體鍵盤和指標裝置使用應用程式,能夠與使用觸控螢幕一樣,完成相同的工作。

必要條件

  • 具備使用 Compose 建構應用程式的經驗
  • 具備 Kotlin 的基本知識,包括 lambda 和協同程式

建構內容

在以 Jetpack Compose 為基礎的應用程式中新增實體鍵盤和滑鼠的支援功能,相關步驟包括:

  1. 按照大螢幕應用程式品質指南中定義的條件檢查應用程式
  2. 查看稽核結果,找出與實體鍵盤和滑鼠支援相關的問題
  3. 修正問題

更具體來說,您將更新範例應用程式的下列項目:

  • 鍵盤導覽
  • 上下捲動的鍵盤快速鍵
  • 鍵盤快速鍵協助工具

課程內容

  • 如何稽核應用程式的虛擬裝置支援功能
  • 如何使用 Compose 管理鍵盤導覽功能
  • 如何使用 Compose 新增鍵盤快速鍵

軟硬體需求

  • Android Studio Hedgehog 以上版本
  • 使用下列任一裝置執行範例應用程式:
  • 配備實體鍵盤和滑鼠的大螢幕裝置
  • 在 Desktop 裝置定義類別中含有設定檔的 Android 虛擬裝置

2. 設定

  1. 複製 large-screen-codelabs GitHub 存放區:
git clone https://github.com/android/large-screen-codelabs

您也可以下載並取消封存 large-screen-codelabs ZIP 檔案:

  1. 前往 add-keyboard-and-mouse-support-with-compose 資料夾:
  2. 在 Android Studio 中開啟專案。add-keyboard-and-cursor-support-with-compose 資料夾包含一個專案。
  3. 如果沒有 Android 平板電腦或摺疊式裝置,也沒有配備實體鍵盤和滑鼠的 ChromeOS 裝置,請在 Android Studio 中開啟「Device Manager」,然後在「Desktop」類別中建立任一種虛擬裝置。

Desktop 類別中的虛擬裝置

3. 探索應用程式

範例應用程式會顯示文章清單。使用者可以閱讀從清單選取的文章。

應用程式會根據應用程式的視窗寬度,自動調整版面配置。以下三個視窗類別可分類應用程式的視窗寬度:精簡、中等和展開。

定義視窗寬度的視窗大小類別:精簡、中等和展開。應用程式視窗寬度小於 600 dp 時,會歸類為精簡。視窗寬度大於或等於 640 dp 時,會歸類為展開。凡是不屬於精簡或展開類別的視窗,其視窗大小類別都是中等。

精簡和中等視窗大小類別的版面配置

應用程式採用單一窗格版面配置。在主畫面上,應用程式會顯示文章清單。使用者從清單選取文章時,畫面隨即轉換並顯示該文章。

全域導覽是使用導覽匣實作而成。

應用程式在 Desktop 模擬器的精簡視窗中執行,並顯示文章清單。

展開視窗大小類別的版面配置

應用程式採用清單/詳細資料版面配置。清單窗格顯示文章清單,詳細資料窗格則顯示所選文章。

全域導覽是使用導覽邊欄實作而成。

應用程式以展開視窗大小類別,在 Desktop 模擬器上執行。

4. 背景

Compose 提供多種 API,協助應用程式處理來自實體鍵盤和滑鼠的事件。有些 API 支援的鍵盤和滑鼠事件處理方式,類似於觸控事件的處理方式。因此,在許多用途中,應用程式不必進行任何開發作業,即可支援實體鍵盤和滑鼠。

典型的範例是 clickable 修飾符,可用於偵測點擊。手指輕觸動作會偵測為點擊,滑鼠點選動作和按下 Enter 鍵也會偵測為點擊。如果應用程式透過 clickable 修飾符偵測到點擊,就可讓使用者與相關元件互動,無論輸入裝置為何。

但是,儘管有了這樣的 API 高度支援功能,為了支援實體鍵盤和滑鼠,仍需進行一些開發工作。其中一個原因是,您需要測試應用程式,才能找出極端情況。此外,也必須設法減少因裝置特性造成的使用者操作不便,例如:

  • 使用者不瞭解可以點選哪些元件
  • 使用者無法如預期般移動鍵盤焦點
  • 使用者無法使用實體鍵盤向上或向下捲動

鍵盤焦點

鍵盤焦點是使用者與實體鍵盤和螢幕觸控互動的主要差異。無論先前輕觸的元件位置為何,使用者都可以輕觸畫面上的任何元件。相較之下,如果使用鍵盤,使用者就需要先選取要互動的元件,才能開始實際互動。這項選取作業稱為「鍵盤焦點」

使用者可以利用 Tab 鍵和方向 (或箭頭) 鍵移動鍵盤焦點。根據預設,鍵盤焦點只會移動至鄰近元件

實體鍵盤的操作不便多數與鍵盤焦點有關。以下列出一般問題:

  • 使用者無法將鍵盤焦點移至要互動的元件
  • 使用者按下 Enter 鍵時,元件偵測不到點擊
  • 鍵盤焦點的移動方式與使用者期望不同
  • 使用者需要按多個按鍵,才能將鍵盤焦點移至畫面轉換後想互動的元件
  • 使用者無法判斷哪個元件擁有鍵盤焦點,因為沒有視覺提示指出鍵盤焦點
  • 使用者前往新畫面後,無法判斷預設的聚焦元件

鍵盤焦點的視覺指標十分重要,否則使用者可能會在應用程式中迷失,不瞭解按下 Enter 鍵後會發生什麼事。醒目顯示是常見的視覺提示,用來指出鍵盤焦點。在下圖中,右側資訊卡中的按鈕已醒目顯示,因此使用者可以瞭解該按鈕擁有鍵盤焦點。

53ee7662b764f2dd.png

鍵盤快速鍵

使用者會預期在搭配實體鍵盤使用應用程式時,可以使用常見的鍵盤快速鍵。根據預設,部分元件會啟用標準鍵盤快速鍵。BasicTextField 是典型的例子,可讓使用者使用標準文字編輯鍵盤快速鍵,包括:

快速鍵

功能

Ctrl+C

複製

Ctrl+X

剪下

Ctrl+V

貼上

Ctrl+Z

復原

Ctrl+Y

重做

應用程式可以藉由處理按鍵事件來新增鍵盤快速鍵。您可以使用 onKeyEvent 修飾符和 onPreviewKeyEvent 修飾符監控按鍵事件。

指標裝置:滑鼠、觸控板和觸控筆

應用程式可以相同方式處理滑鼠、觸控板和觸控筆。使用 clickable 修飾符時,觸控板的輕觸動作會偵測為點擊,使用觸控筆的輕觸動作也會偵測為點擊。

讓使用者透過視覺提示瞭解能否點選某個元件,這一點十分重要。因此,大螢幕應用程式品質指南中提到了懸停狀態

Material 3 元件預設支援懸停狀態,並提供懸停狀態的視覺效果。您可以使用 indication 修飾符,將懸停狀態套用至互動式元件。

捲動

可捲動容器預設支援滑鼠滾輪捲動、觸控板的捲動手勢,以及使用 Page upPage down 鍵捲動。

如果是水平捲動,只要應用程式在懸停狀態下顯示左右箭頭按鈕,使用者就能點按按鈕來捲動內容,因此非常容易使用。

17feb4d3bf08831e.png

依裝置連接和卸除狀態變更設定

使用者可以在應用程式執行期間連接或卸除鍵盤和滑鼠。如果使用者看到要輸入大量文字的文字欄位,可能會連接實體鍵盤。透過藍牙連線的滑鼠進入休眠模式時會中斷連線。透過 USB 連接的鍵盤可能會意外卸除。

連接或卸除週邊硬體會觸發設定變更。在設定變更期間,應用程式應保留原本的狀態。詳情請參閱「儲存 UI 狀態」。

5. 使用鍵盤和滑鼠檢查範例應用程式

如要開始進行實體鍵盤和滑鼠支援的開發作業,請啟動範例應用程式並確認下列事項:

  • 使用者應該要能將鍵盤焦點移至所有互動式元件
  • 使用者應該要能使用 Enter 鍵「點選」聚焦的元件
  • 互動式元件應該在取得鍵盤焦點時顯示指標
  • 鍵盤焦點可如使用者預期 (亦即根據既定慣例),使用 Tab 鍵、Shift+Tab 鍵和方向 (箭頭) 鍵移動
  • 互動式元件應有懸停狀態
  • 使用者應該要能點選互動式元件
  • 在適當的元件上按右鍵 (次要控制點擊) 會顯示內容選單,例如在長按或選取文字時會顯示內容選單的元件

您應瀏覽本程式碼研究室中的所有項目兩次:一次是針對單一窗格版面配置,另一次則是針對清單/詳細資料版面配置。

本程式碼研究室中需要修正的問題

您應該會發現有問題。在本程式碼研究室中,您將修正下列問題:

  • 使用者無法向下捲動文章,因此無法單單使用實體鍵盤閱讀整篇文章
  • 使用者無法判斷詳細資料窗格是否擁有鍵盤焦點

6. 讓使用者能夠在詳細資料窗格中閱讀整篇文章

詳細資料窗格會顯示所選文章。有些文章太長,不捲動就無法閱讀整篇文章。但是,使用者無法單單使用實體鍵盤上下捲動文章。

4627289223e5cfbc.gif

可捲動容器 (例如 LazyColumn) 可讓使用者利用 Page down 鍵向下捲動。這個問題的根本原因是,使用者無法將鍵盤焦點移至詳細資料窗格。

元件應該要能取得鍵盤焦點,接收鍵盤事件。focusable 修飾符可讓被修飾的元件取得鍵盤焦點。

如要修正這個問題,請按照下列步驟操作:

  1. 存取 ui/article/PostContent.kt 檔案中的 PostContent 可組合函式
  2. 使用 focusable 修飾符修飾 LazyColumn 可組合函式
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .focusable(),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

指出文章擁有鍵盤焦點

現在,使用者可以按 Page down 鍵向下捲動,閱讀整篇文章。不過,他們難以瞭解 PostContent 元件是否擁有鍵盤焦點,因為沒有任何能指出鍵盤焦點的視覺效果。

應用程式可以將 Indication 與元件建立關聯,以視覺方式指出鍵盤焦點。Indication 會建立物件,根據互動行為顯示視覺效果。舉例來說,Material 3 預設的 Indication 會醒目顯示擁有鍵盤焦點的元件。

範例應用程式含有名為 BorderIndicationIndication。這個指標會在擁有鍵盤焦點的元件旁邊顯示一條線 (如以下螢幕截圖所示),相關程式碼儲存在 ui/components/BorderIndication.kt 檔案中。

文章擁有鍵盤焦點時,旁邊會顯示淺灰色線。

如要讓 PostConent 可組合函式在擁有鍵盤焦點時顯示 BorderIndication,請按照下列步驟操作:

  1. 存取 ui/article/PostContent.kt 檔案中的 PostContent 可組合函式
  2. 宣告與 remember() 函式的傳回值相關聯的 interactionSource
  3. remember() 函式中呼叫 MutableInteractionSource() 函式,將建立的 MutableInteractionSource 物件與 interactionSource 值建立關聯
  4. 使用 interactionSource 參數,將 interactionSource 值傳遞至 focusable 修飾符
  5. 變更 PostContent 可組合函式的修飾符,在叫用 indication 修飾符後呼叫 focusable 修飾符
  6. interactionSource 值和 BorderIndication 函式的傳回值傳遞至指標修飾符
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

新增用於上下捲動的鍵盤快速鍵

使用者可以使用 Spacebar 鍵上下捲動,這是常見的功能。應用程式可以藉由新增鍵盤快速鍵來實作這項功能,如下表所示:

快速鍵

功能

Spacebar

向下捲動文章

Shift + Spacebar

向上捲動文章

應用程式可以使用 onKeyEvent 修飾符,處理在被修飾元件上發生的按鍵事件。這個修飾符接受 lambda,您可以使用描述按鍵事件的 KeyEvent 物件呼叫此 lambda。lambda 應傳回 Boolean 值,指出是否已取用按鍵事件。

系統會將 LazyColumnLazyRow 的捲動位置擷取到 LazyListState 物件中。應用程式可透過 LazyListState 物件呼叫 animateScrollBy() 暫停方法,觸發捲動動作。這個方法會按照指定的像素數向下捲動 LazyColumn。如果使用負浮點值呼叫暫停函式,函式會向上捲動 LazyColumn

如要實作這些鍵盤快速鍵,請按照下列步驟操作:

  1. 存取 ui/article/PostContent.kt 檔案中的 PostContent 可組合函式
  2. 使用 onKeyEvent 修飾符修飾 LazyColumn 可組合函式
  3. 在傳遞至 onKeyEvent 修飾符的 lambda 中新增 if 運算式,如下所示:
  • 如果滿足下列條件,傳回 true
  • 已按下 Spacebar 鍵。只要測試 type 屬性是否為 KeyType.KeyDown,以及 key 屬性是否為 Key.Spacebar,即可偵測到這個按鍵的情況
  • isCtrlPressed 屬性為 false,確保沒有按下 Ctrl
  • isAltPressed 屬性為 false,確保沒有按下 Alt
  • isMetaPressed 屬性為 false,確保沒有按下 Meta 鍵 (請參閱附註)
  • 其他情況則傳回 false
  1. 判斷使用 Spacebar 的捲動量,如下所示:
  • 按下 Shift 鍵時為 -0.4f,如指定 KeyEvent 物件的 isShiftPressed 屬性所描述
  • 其他情況則為 0.4f
  1. 透過 coroutineScope 呼叫 launch() 方法,這是 PostContent 可組合函式的參數
  2. 將上一步計算的相對捲動量與 launch 方法 lambda 參數中的 state.layoutInfo.viewportSize.height 屬性相乘,算出實際捲動量。這項屬性代表 PostContent 可組合函式中呼叫的 LazyColumn 高度。
  3. launch() 方法的 lambda 中呼叫 state.animateScrollBy() 方法,即可觸發垂直捲動
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .onKeyEvent {
                if (
                    it.type == KeyEventType.KeyDown &&
                    it.key == Key.Spacebar &&
                    !it.isCtrlPressed &&
                    !it.isAltPressed &&
                    !it.isMetaPressed
                ) {

                    val relativeAmount = if (it.isShiftPressed) {
                        -0.4f
                    } else {
                        0.4f
                    }
                    coroutineScope.launch {
                        state.animateScrollBy(relativeAmount * state.layoutInfo.viewportSize.height)
                    }
                    true
                } else {
                    false
                }
            }
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

告知使用者鍵盤快速鍵

除非使用者知道鍵盤快速鍵,否則他們無法充分利用新增的鍵盤。應用程式可以透過 Android 系統 UI 中的鍵盤快速鍵輔助程式,告知使用者可用的快速鍵。使用者可以使用 Meta+/ 鍵開啟快速鍵輔助程式。

鍵盤快速鍵輔助程式會顯示上一節中新增的鍵盤快速鍵。

應用程式會覆寫應用程式主要活動中的 onProvideKeyboardShortcuts() 方法,為鍵盤快速鍵輔助程式提供鍵盤快速鍵清單。

具體來說,應用程式會將多個 KeyboardShortcutGroup 物件加進傳遞至 onProvideKeyboardShortcuts() 的可變動清單,提供這些物件。每個 KeyboardShortcutGroup 代表鍵盤快速鍵的已命名類別,可讓應用程式依用途或情境將可用鍵盤快速鍵分組。

範例應用程式有兩個鍵盤快速鍵:SpacebarShift+Spacebar

如要在鍵盤快速鍵輔助程式中提供這兩個快速鍵,請按照下列步驟操作:

  1. 開啟 MainActivity.kt 檔案
  2. 覆寫 MainActivity 中的 onProvideKeyboardShortcuts() 方法
  3. 確認 Android SDK 為 Android 7.0 (API 級別 24) 以上版本,以便使用鍵盤快速鍵輔助程式
  4. 確認方法的第一個參數「不是」null
  5. 使用下列參數為 Spacebar 鍵建立 KeyboardShortcutInfo 物件:
  • 說明文字
  • android.view.KeyEvent.KEYCODE_SPACE
  • 0 (表示沒有修飾符)
  1. 使用下列參數為 Shift+Spacebar 建立另一個 KeyboardShortcutInfo
  • 說明文字
  • android.view.KeyEvent.KEYCODE_SPACE
  • android.view.KeyEvent.META_SHIFT_ON
  1. 建立包含這兩個 KeyboardShortcutInfo 物件的不可變動清單
  2. 使用以下參數建立 KeyboardShortcutGroup 物件:
  • 文字形式的群組名稱
  • 上一步中的不可變動清單
  1. KeyboardShortcutGroup 物件新增至可變動清單,做為 onProvideKeyboardShortcuts() 方法傳遞的第一個參數

覆寫的方法如下所示:

   override fun onProvideKeyboardShortcuts(
        data: MutableList<KeyboardShortcutGroup>?,
        menu: Menu?,
        deviceId: Int
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && data != null) {
            val shortcutGroup = KeyboardShortcutGroup(
                "To read articles",
                listOf(
                    KeyboardShortcutInfo("Scroll down", KeyEvent.KEYCODE_SPACE, 0), // 0 means no modifier key is pressed
                    KeyboardShortcutInfo("Scroll up", KeyEvent.KEYCODE_SPACE, KeyEvent.META_SHIFT_ON),
                )
            )
            data.add(shortcutGroup)
        }
    }

開始執行

現在,使用者只要使用 Spacebar 鍵捲動文章即可閱讀整篇文章。如要試用這項功能,請使用 Tab 鍵或方向鍵,將鍵盤焦點移到文章上。畫面上會顯示鼓勵您按下 Spacebar 鍵的訊息。

鍵盤快速鍵輔助程式會顯示您新增的兩個鍵盤快速鍵 (按下 Meta+/ 鍵)。新增的快速鍵會列在「Current app」分頁中。

7. 加快詳細資料窗格中的鍵盤導覽速度

應用程式以展開視窗大小類別執行時,使用者需要按下 Tab 鍵數次,才能將鍵盤焦點移至詳細資料窗格。如果使用正確的方向鍵,使用者只需一個動作,就能將鍵盤焦點從文章清單移到文章,但仍然需要移動鍵盤焦點。初期焦點並不支援使用者閱讀文章的主要目標。

應用程式可以使用 FocusRequester 物件,要求將鍵盤焦點移至特定元件。focusRequester 修飾符會將 FocusRequester 物件與被修飾的元件建立關聯。應用程式可以呼叫 FocusRequester 物件的 requestFocus() 方法,傳送焦點移動的實際要求。

傳送移動鍵盤焦點的要求,是元件的連帶效果。應用程式應該使用 LaunchedEffect 函式,以正確的方式呼叫該方法。

如要設定 PostContent 可組合函式,在使用者從文章清單選取文章時取得鍵盤焦點,請按照下列步驟操作:

  1. 存取 ui/article/ PostContent.kt 檔案中的 PostContent 可組合函式。
  2. 使用 focusRequester 修飾符,將 focusRequester 值與 LazyColumn 可組合函式建立關聯。focusRequester 值已指定為 PostContent 可組合函式的選用參數。
  3. 使用 post (PostContent 可組合函式的第一個參數) 呼叫 LaunchedEffect,在使用者選取文章時呼叫傳遞的 lambda。
  4. 在傳遞至 LaunchedEffect 函式的 lambda 中呼叫 focusRequester.requestFocus() 方法。

更新後的 PostContent 可組合函式如下所示:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LaunchedEffect(post) {
        focusRequester.requestFocus()
    }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .onKeyEvent {
                if (it.type == KeyEventType.KeyDown && it.key == Key.Spacebar) {
                    val relativeAmount = if (it.isShiftPressed) {
                        -0.4f
                    } else {
                        0.4f
                    }
                    coroutineScope.launch {
                        state.animateScrollBy(relativeAmount * state.layoutInfo.viewportSize.height)
                    }
                    true
                } else {
                    false
                }
            }
            .focusRequester(focusRequester),
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

開始執行

現在,如果使用者從文章清單選擇文章,鍵盤焦點就會移至文章。選擇文章時,您會看到相關訊息,鼓勵您使用 Spacebar 鍵向下捲動文章。

8. 恭喜

非常好!您已在範例應用程式中新增實體鍵盤和滑鼠支援功能。因此,使用者可以從文章清單選取文章,並且單單使用實體鍵盤或滑鼠就能閱讀所選文章。

您已學到新增實體鍵盤和滑鼠支援功能所需的下列知識:

  • 如何檢查應用程式是否支援實體鍵盤和滑鼠,包括使用模擬器
  • 如何使用 Compose 管理鍵盤導覽功能
  • 如何使用 Compose 新增鍵盤快速鍵

此外,您也稍微修改了程式碼,新增實體鍵盤和滑鼠支援功能。

您現在可以使用 Compose,在正式版應用程式中新增實體鍵盤和滑鼠支援功能了。

再進一步學習,即可為下列功能新增鍵盤快速鍵:

  • 將所選文章標示為喜歡。
  • 將所選文章加入書籤。
  • 將所選文章分享給其他應用程式。

瞭解詳情