處理輸入內容變更

Chromebook 為使用者提供多種輸入選項:鍵盤、滑鼠、觸控板、觸控螢幕、觸控筆、MIDI,以及遊戲手把/藍牙控制器。也就是說,同一部裝置可以成為 DJ 的工作站、藝術家的畫布,或是遊戲玩家偏好的 AAA 串流遊戲平台。

開發人員可藉此為使用者打造多樣化的精彩應用程式體驗,充分運用他們手邊的輸入裝置,包括外接鍵盤、觸控筆和 Stadia 遊戲控制器。不過,所有這些可能性也需要您考慮使用者介面,確保應用程式體驗流暢且符合邏輯。如果您的應用程式或遊戲是專為手機設計,就更是如此。舉例來說,如果遊戲提供手機專用的螢幕觸控式搖桿,使用者以鍵盤操作時,您可能想隱藏這項控制項。

本頁面將說明考量多個輸入來源時的主要問題,以及解決這些問題的策略。

使用者探索支援的輸入法

理想情況下,應用程式會順暢回應使用者選擇的任何輸入方式。通常這很簡單,不需要提供額外資訊給使用者。舉例來說,使用者以滑鼠、觸控板、觸控螢幕、觸控筆等點選按鈕時,按鈕應能正常運作,您不需要告知使用者可使用這些不同裝置啟動按鈕。

不過,在某些情況下,輸入方式可以改善使用者體驗,因此讓使用者知道應用程式支援該方式可能很有意義。以下提供一些例子:

  • 媒體播放應用程式可能支援許多實用的鍵盤快速鍵,但使用者可能不容易猜到。
  • 如果您建立的是 DJ 應用程式,使用者一開始可能會使用觸控螢幕,而未意識到您允許他們使用鍵盤/觸控板,以觸覺方式存取部分功能。同樣地,他們可能不知道你支援多種 MIDI DJ 控制器,因此鼓勵他們查看支援的硬體,或許能帶來更真實的 DJ 體驗。
  • 您的遊戲可能很適合使用觸控螢幕和鍵盤/滑鼠操作,但使用者可能不知道遊戲也支援多種藍牙遊戲控制器。連結其中一項服務可提高使用者滿意度和參與度。

您可以在適當時間傳送訊息,協助使用者探索輸入選項。每個應用程式的實作方式都不一樣,以下列舉幾個範例:

  • 首次執行或每日提示彈出式視窗
  • 設定面板中的設定選項可被動向使用者指出支援的存在。舉例來說,如果遊戲的設定面板中顯示「遊戲控制器」分頁,表示遊戲支援控制器。
  • 內容相關訊息。舉例來說,如果系統偵測到實體鍵盤,且使用者正透過滑鼠或觸控螢幕點選動作,您或許可以顯示實用提示,建議使用鍵盤快速鍵。
  • 鍵盤快速鍵清單。偵測到實體鍵盤時,在 UI 中顯示可用鍵盤快速鍵清單,可同時達到宣傳鍵盤支援功能,以及讓使用者輕鬆查看及記住支援的快速鍵這兩個目的。

輸入變化的 UI 標籤/版面配置

理想情況下,如果使用不同的輸入裝置,視覺介面不應需要大幅變更,所有可能的輸入都應「正常運作」。不過,有幾個重要的例外狀況。其中兩項主要功能是觸控專屬 UI 和螢幕上的提示。

觸控專用 UI

只要應用程式有觸控專屬的 UI 元素 (例如遊戲中的螢幕搖桿),您就應考慮未使用觸控時的使用者體驗。在某些行動遊戲中,螢幕上會顯示觸控操作所需的控制項,但如果使用者是透過遊戲搖桿或鍵盤玩遊戲,這些控制項就沒有必要。應用程式或遊戲應提供邏輯,偵測目前使用的輸入法,並據此調整 UI。如需相關範例,請參閱下方的實作一節

賽車遊戲 UI - 一個顯示螢幕上的控制選項,另一個顯示鍵盤

畫面上的提示

應用程式可能會向使用者提供實用的螢幕提示。請注意,這些手勢不適用於輸入裝置。例如:

  • 滑動即可…
  • 輕觸任一處即可關閉
  • 雙指撥動縮放
  • 按下「X」可執行下列操作:
  • 長按即可啟用

部分應用程式可能會調整字詞,以支援各種輸入方式。在適當情況下,建議採用這種做法,但在許多情況下,具體性很重要,您可能需要根據使用的輸入法顯示不同訊息,尤其是在應用程式首次執行時的教學模式中。

多種語言注意事項

如果應用程式支援多種語言,請仔細思考字串架構。舉例來說,如果您支援 3 種輸入模式和 5 種語言,可能表示每個 UI 訊息都有 15 種不同版本。這會增加新增功能所需的工作量,並提高拼字錯誤的機率。

其中一種方法是將介面動作視為一組獨立的字串。舉例來說,如果您將「關閉」動作定義為自己的字串變數,並提供輸入專屬的變體,例如:「輕觸任意位置即可關閉」、「按下『Esc』鍵即可關閉」、「按一下任意位置即可關閉」、「按下任意按鈕即可關閉」,那麼所有需要告知使用者如何關閉項目的 UI 訊息,都可以使用這個單一的「關閉」字串變數。輸入法變更時,只要變更這個變數的值即可。

螢幕鍵盤 / 輸入法編輯器輸入

請注意,如果使用者在沒有實體鍵盤的情況下使用應用程式,可以透過螢幕小鍵盤輸入文字。請務必測試螢幕小鍵盤出現時,必要的 UI 元素是否不會遭到遮蔽。詳情請參閱「Android IME 可見性說明文件」。

導入作業

在大多數情況下,無論畫面上顯示什麼內容,應用程式或遊戲都應正確回應所有有效輸入。如果使用者使用觸控螢幕 10 分鐘,但突然改用鍵盤,無論螢幕上的視覺提示或控制項為何,鍵盤輸入都應正常運作。換句話說,功能應優先於視覺效果/文字。即使輸入偵測邏輯發生錯誤或出現意外狀況,這也有助於確保應用程式/遊戲仍可使用。

下一步是顯示目前所用輸入法的正確 UI。如何準確偵測?如果使用者在應用程式執行期間切換輸入法,會發生什麼情況?如果他們同時使用多種方法,該怎麼辦?

根據收到的事件,優先處理狀態機器

其中一種做法是追蹤目前的「有效輸入狀態」,也就是目前顯示在螢幕上供使用者輸入的提示,方法是追蹤應用程式收到的實際輸入事件,並使用優先順序邏輯在狀態之間轉換。

優先順序

為什麼要優先處理輸入狀態?使用者與裝置互動的方式五花八門,您的應用程式應支援他們選擇的互動方式。舉例來說,如果使用者選擇同時使用觸控螢幕和藍牙滑鼠,系統應支援這項操作。但要顯示哪些螢幕上的輸入提示和控制項?滑鼠或觸控?

將每組提示定義為「輸入狀態」,然後排定優先順序,有助於以一致的方式做出這項決定。

收到的輸入事件會決定狀態

為什麼只對收到的輸入事件採取行動?您可能會想追蹤藍牙連線,判斷是否已連接藍牙控制器,或監控 USB 連線,判斷是否已連接 USB 裝置。基於兩個主要原因,我們不建議採用這種做法。

首先,您可根據 API 變數推測連線裝置的資訊並不一致,且藍牙/硬體/觸控筆裝置的數量持續增加。

使用接收到的事件而非連線狀態的第二個原因是,使用者可能已連線滑鼠、藍牙控制器、MIDI 控制器等,但並未主動使用這些裝置與應用程式或遊戲互動。

回應應用程式主動接收的輸入事件,可確保您即時回應使用者的實際動作,而不是根據不完整的資訊猜測意圖。

示例:支援觸控、鍵盤/滑鼠和控制器的遊戲

假設您開發了一款適用於觸控式手機的賽車遊戲,玩家可以加速、減速、右轉、左轉,或使用氮氣加速。

目前的觸控螢幕介面包含螢幕左下方的螢幕搖桿,可控制速度和方向,以及右下方的按鈕,可使用氮氣加速。使用者可以在賽道上收集氮氣罐,當畫面底部的氮氣條全滿時,按鈕上方會顯示「按下可使用氮氣!」訊息。如果是使用者第一次玩遊戲,或一段時間未收到任何輸入內容,搖桿上方會顯示「教學」文字,向使用者說明如何移動車輛。

您想新增鍵盤和藍牙遊戲控制器支援功能。該從何著手?

可使用觸控操作的賽車遊戲

輸入狀態

首先,請找出遊戲可能執行的所有輸入狀態,然後列出您想在每個狀態中變更的所有參數。

                                                                                                                                                                        
觸控鍵盤/滑鼠遊戲控制器
       

回應

     
       

所有輸入內容

     
       

所有輸入內容

     
       

所有輸入內容

     
       

畫面上的控制項

     
       

- 螢幕搖桿
- Nitro 按鈕

     
       

- 沒有搖桿
- 沒有氮氣按鈕

     
       

- 沒有搖桿
- 沒有氮氣按鈕

     
       

文字

     
       

輕觸即可取得 Nitro!

     
       

按下「N」鍵即可使用 Nitro!

     
       

按下「A」鍵即可使用 Nitro!

     
       

教學課程

     
       

圖片:搖桿,用於控制速度/方向

     
       

圖片:方向鍵和 WASD 鍵,用於控制速度/方向

     
       

圖片:遊戲手把,用於控制速度/方向

     

追蹤「有效輸入」狀態,然後根據該狀態視需要更新 UI。

注意:請注意,無論遊戲/應用程式處於何種狀態,都應回應所有輸入方式。舉例來說,如果使用者開車時使用觸控螢幕,但按下鍵盤上的 N 鍵,就應觸發氮氣加速動作。

優先狀態變更

部分使用者可能會同時使用觸控螢幕和鍵盤輸入。有些使用者可能會先在平板模式下使用遊戲/應用程式,然後切換到使用鍵盤。其他玩家可能會在遊戲中途連線或中斷連線遊戲控制器。

理想情況下,您不希望出現錯誤的 UI 元素,例如在使用者使用觸控螢幕時,告知他們按下 N 鍵。同時,如果使用者同時使用多個輸入裝置 (例如觸控螢幕和鍵盤),您不希望 UI 在這兩種狀態之間不斷來回切換。

其中一種做法是建立輸入類型優先順序,並在狀態變更之間加入延遲。以上述賽車遊戲為例,您一律要確保在收到螢幕觸控事件時,螢幕上的搖桿會顯示出來,否則使用者可能會覺得遊戲無法操作。這樣觸控螢幕就會成為優先順序最高的裝置。

如果同時收到鍵盤和觸控螢幕事件,遊戲應維持觸控螢幕模式,但仍會對鍵盤輸入做出反應。如果 5 秒後未收到觸控螢幕輸入內容,但仍收到鍵盤事件,螢幕上的控制項可能會淡出,遊戲也會移至鍵盤狀態。

遊戲控制器輸入內容的模式也類似:控制器 UI 狀態的優先順序會低於鍵盤/滑鼠和觸控,且只會在收到遊戲控制器輸入內容 (而非其他形式的輸入內容) 時顯示。遊戲一律會回應控制器輸入內容。

以下是範例的狀態圖和轉換表。根據應用程式或遊戲調整想法。

優先狀態機 - 觸控螢幕、鍵盤/滑鼠、遊戲控制器

                                                                                                                                        
#1 觸控螢幕#2 鍵盤#3 遊戲手把
       

移至 #1

     
       

不適用

     
       

- 收到觸控輸入
- 立即移至觸控輸入狀態

     
       

- 收到觸控輸入
- 立即移至觸控輸入狀態

     
       

移至 #2

     
       

- 5 秒內未觸控
- 收到鍵盤輸入
- 移至鍵盤輸入狀態

     
       

不適用

     
       

- 收到鍵盤輸入內容
(立即移至「鍵盤輸入」狀態)

     
       

移至 #3

     
       

- 5 秒內未觸控
- 5 秒內未使用鍵盤
- 收到遊戲手把輸入內容
- 移至遊戲手把輸入狀態

     
       

- No keyboard for 5s
- Gamepad input received
- Move to Gamepad input state

     
       

不適用

     

注意:請注意,優先順序有助於明確指出應以哪種輸入類型為主。輸入狀態會立即「向上」移動優先順序:

3. 遊戲手把 -> 2. 鍵盤 -> 1. Touch

但優先順序會緩慢「下降」,只有在延遲一段時間後,且低優先順序裝置正在使用中時才會下降。

輸入事件

以下是範例程式碼,說明如何使用標準 Android API 偵測各種輸入裝置的輸入事件。如上所示,您可以使用這些事件來驅動狀態機器。您應根據預期的輸入事件類型,以及應用程式或遊戲調整一般概念。

鍵盤和控制器按鈕

// Drive the state machine based on the received input type
// onKeyDown drives the state machine, but does not trigger game actions
// Both keyboard and game controller events come through as key events
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if (event != null) {
        // Check input source
        val outputMessage = when (event.source) {
            SOURCE_KEYBOARD -> {
                MyStateMachine.KeyboardEventReceived()
                "Keyboard event"
            }
            SOURCE_GAMEPAD -> {
                MyStateMachine.ControllerEventReceived()
                "Game controller event"
            }
            else -> "Other key event: ${event.source}"
        }
        // Do something based on source type
        findViewById(R.id.text_message).text = outputMessage
    }
    // Pass event up to system
    return super.onKeyDown(keyCode, event)
}
// Trigger game events based on key release
// Both keyboard and game controller events come through as key events
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
   when(keyCode) {
       KeyEvent.KEYCODE_N -> {
           MyStateMachine.keyboardEventReceived()
           engageNitro()
           return true // event handled here, return true
       }
   }
   // If event not handled, pass up to system
   return super.onKeyUp(keyCode, event)
}

注意:針對 KeyEvents,您可以選擇使用 onKeyDown()onKeyUp()。在這裡,onKeyDown() 用於控制狀態機器,而 onKeyUp() 則用於觸發遊戲事件。

如果使用者按住按鈕,onKeyUp() 只會在每次按鍵時觸發一次,而 onKeyDown() 則會多次呼叫。如要對按下動作做出反應,請在 onKeyDown() 中處理遊戲事件,並實作邏輯來處理重複事件。詳情請參閱「處理鍵盤動作」說明文件。

觸控和觸控筆

// Touch and stylus events come through as touch events
override fun onTouchEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Get tool type
       val pointerIndex = event.action and ACTION_POINTER_INDEX_MASK shr ACTION_POINTER_INDEX_SHIFT
       val toolType = event.getToolType(pointerIndex)

       // Check tool type
       val outputMessage = when (toolType) {
           TOOL_TYPE_FINGER -> {
               MyStateMachine.TouchEventReceived()
               "Touch event"
           }
           TOOL_TYPE_STYLUS -> {
                MyStateMachine.StylusEventReceived()
               "Stylus event"
           }
           else -> "Other touch event: ${toolType}"
       }

       // Do something based on tool type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}

滑鼠和搖桿

// Mouse and joystick events come through as generic events
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Check input source
       val outputMessage = when (event.source) {
           SOURCE_JOYSTICK -> {
                MyStateMachine.ControllerEventReceived()
               "Controller event"
           }
           SOURCE_MOUSE -> {
                MyStateMachine.MouseEventReceived()
               "Mouse event"
           }
           else -> "Other generic event: ${event.source}"
       }
       // Do something based on source type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}