Chromebook 為使用者提供多種輸入選項:鍵盤、滑鼠、觸控板、觸控螢幕、觸控筆、MIDI,以及遊戲手把/藍牙控制器。也就是說,同一部裝置可以成為 DJ 的工作站、藝術家的畫布,或是遊戲玩家偏好的 AAA 串流遊戲平台。
開發人員可藉此為使用者打造多樣化的精彩應用程式體驗,充分運用他們手邊的輸入裝置,包括外接鍵盤、觸控筆和 Stadia 遊戲控制器。不過,所有這些可能性也需要您考慮使用者介面,確保應用程式體驗流暢且符合邏輯。如果您的應用程式或遊戲是專為手機設計,就更是如此。舉例來說,如果遊戲提供手機專用的螢幕觸控式搖桿,使用者以鍵盤操作時,您可能想隱藏這項控制項。
本頁面將說明考量多個輸入來源時的主要問題,以及解決這些問題的策略。
使用者探索支援的輸入法
理想情況下,應用程式會順暢回應使用者選擇的任何輸入方式。通常這很簡單,不需要提供額外資訊給使用者。舉例來說,使用者以滑鼠、觸控板、觸控螢幕、觸控筆等點選按鈕時,按鈕應能正常運作,您不需要告知使用者可使用這些不同裝置啟動按鈕。
不過,在某些情況下,輸入方式可以改善使用者體驗,因此讓使用者知道應用程式支援該方式可能很有意義。以下提供一些例子:
- 媒體播放應用程式可能支援許多實用的鍵盤快速鍵,但使用者可能不容易猜到。
- 如果您建立的是 DJ 應用程式,使用者一開始可能會使用觸控螢幕,而未意識到您允許他們使用鍵盤/觸控板,以觸覺方式存取部分功能。同樣地,他們可能不知道你支援多種 MIDI DJ 控制器,因此鼓勵他們查看支援的硬體,或許能帶來更真實的 DJ 體驗。
- 您的遊戲可能很適合使用觸控螢幕和鍵盤/滑鼠操作,但使用者可能不知道遊戲也支援多種藍牙遊戲控制器。連結其中一項服務可提高使用者滿意度和參與度。
您可以在適當時間傳送訊息,協助使用者探索輸入選項。每個應用程式的實作方式都不一樣,以下列舉幾個範例:
- 首次執行或每日提示彈出式視窗
- 設定面板中的設定選項可被動向使用者指出支援的存在。舉例來說,如果遊戲的設定面板中顯示「遊戲控制器」分頁,表示遊戲支援控制器。
- 內容相關訊息。舉例來說,如果系統偵測到實體鍵盤,且使用者正透過滑鼠或觸控螢幕點選動作,您或許可以顯示實用提示,建議使用鍵盤快速鍵。
- 鍵盤快速鍵清單。偵測到實體鍵盤時,在 UI 中顯示可用鍵盤快速鍵清單,可同時達到宣傳鍵盤支援功能,以及讓使用者輕鬆查看及記住支援的快速鍵這兩個目的。
輸入變化的 UI 標籤/版面配置
理想情況下,如果使用不同的輸入裝置,視覺介面不應需要大幅變更,所有可能的輸入都應「正常運作」。不過,有幾個重要的例外狀況。其中兩項主要功能是觸控專屬 UI 和螢幕上的提示。
觸控專用 UI
只要應用程式有觸控專屬的 UI 元素 (例如遊戲中的螢幕搖桿),您就應考慮未使用觸控時的使用者體驗。在某些行動遊戲中,螢幕上會顯示觸控操作所需的控制項,但如果使用者是透過遊戲搖桿或鍵盤玩遊戲,這些控制項就沒有必要。應用程式或遊戲應提供邏輯,偵測目前使用的輸入法,並據此調整 UI。如需相關範例,請參閱下方的實作一節。
畫面上的提示
應用程式可能會向使用者提供實用的螢幕提示。請注意,這些手勢不適用於輸入裝置。例如:
- 滑動即可…
- 輕觸任一處即可關閉
- 雙指撥動縮放
- 按下「X」可執行下列操作:
- 長按即可啟用
部分應用程式可能會調整字詞,以支援各種輸入方式。在適當情況下,建議採用這種做法,但在許多情況下,具體性很重要,您可能需要根據使用的輸入法顯示不同訊息,尤其是在應用程式首次執行時的教學模式中。
多種語言注意事項
如果應用程式支援多種語言,請仔細思考字串架構。舉例來說,如果您支援 3 種輸入模式和 5 種語言,可能表示每個 UI 訊息都有 15 種不同版本。這會增加新增功能所需的工作量,並提高拼字錯誤的機率。
其中一種方法是將介面動作視為一組獨立的字串。舉例來說,如果您將「關閉」動作定義為自己的字串變數,並提供輸入專屬的變體,例如:「輕觸任意位置即可關閉」、「按下『Esc』鍵即可關閉」、「按一下任意位置即可關閉」、「按下任意按鈕即可關閉」,那麼所有需要告知使用者如何關閉項目的 UI 訊息,都可以使用這個單一的「關閉」字串變數。輸入法變更時,只要變更這個變數的值即可。
螢幕鍵盤 / 輸入法編輯器輸入
請注意,如果使用者在沒有實體鍵盤的情況下使用應用程式,可以透過螢幕小鍵盤輸入文字。請務必測試螢幕小鍵盤出現時,必要的 UI 元素是否不會遭到遮蔽。詳情請參閱「Android IME 可見性說明文件」。
導入作業
在大多數情況下,無論畫面上顯示什麼內容,應用程式或遊戲都應正確回應所有有效輸入。如果使用者使用觸控螢幕 10 分鐘,但突然改用鍵盤,無論螢幕上的視覺提示或控制項為何,鍵盤輸入都應正常運作。換句話說,功能應優先於視覺效果/文字。即使輸入偵測邏輯發生錯誤或出現意外狀況,這也有助於確保應用程式/遊戲仍可使用。
下一步是顯示目前所用輸入法的正確 UI。如何準確偵測?如果使用者在應用程式執行期間切換輸入法,會發生什麼情況?如果他們同時使用多種方法,該怎麼辦?
根據收到的事件,優先處理狀態機器
其中一種做法是追蹤目前的「有效輸入狀態」,也就是目前顯示在螢幕上供使用者輸入的提示,方法是追蹤應用程式收到的實際輸入事件,並使用優先順序邏輯在狀態之間轉換。
優先順序
為什麼要優先處理輸入狀態?使用者與裝置互動的方式五花八門,您的應用程式應支援他們選擇的互動方式。舉例來說,如果使用者選擇同時使用觸控螢幕和藍牙滑鼠,系統應支援這項操作。但要顯示哪些螢幕上的輸入提示和控制項?滑鼠或觸控?
將每組提示定義為「輸入狀態」,然後排定優先順序,有助於以一致的方式做出這項決定。
收到的輸入事件會決定狀態
為什麼只對收到的輸入事件採取行動?您可能會想追蹤藍牙連線,判斷是否已連接藍牙控制器,或監控 USB 連線,判斷是否已連接 USB 裝置。基於兩個主要原因,我們不建議採用這種做法。
首先,您可根據 API 變數推測連線裝置的資訊並不一致,且藍牙/硬體/觸控筆裝置的數量持續增加。
使用接收到的事件而非連線狀態的第二個原因是,使用者可能已連線滑鼠、藍牙控制器、MIDI 控制器等,但並未主動使用這些裝置與應用程式或遊戲互動。
回應應用程式主動接收的輸入事件,可確保您即時回應使用者的實際動作,而不是根據不完整的資訊猜測意圖。
示例:支援觸控、鍵盤/滑鼠和控制器的遊戲
假設您開發了一款適用於觸控式手機的賽車遊戲,玩家可以加速、減速、右轉、左轉,或使用氮氣加速。
目前的觸控螢幕介面包含螢幕左下方的螢幕搖桿,可控制速度和方向,以及右下方的按鈕,可使用氮氣加速。使用者可以在賽道上收集氮氣罐,當畫面底部的氮氣條全滿時,按鈕上方會顯示「按下可使用氮氣!」訊息。如果是使用者第一次玩遊戲,或一段時間未收到任何輸入內容,搖桿上方會顯示「教學」文字,向使用者說明如何移動車輛。
您想新增鍵盤和藍牙遊戲控制器支援功能。該從何著手?
輸入狀態
首先,請找出遊戲可能執行的所有輸入狀態,然後列出您想在每個狀態中變更的所有參數。
| 觸控 | 鍵盤/滑鼠 | 遊戲控制器 | |
|---|---|---|---|
|
回應 |
所有輸入內容 |
所有輸入內容 |
所有輸入內容 |
|
畫面上的控制項 |
- 螢幕搖桿 |
- 沒有搖桿 |
- 沒有搖桿 |
|
文字 |
輕觸即可取得 Nitro! |
按下「N」鍵即可使用 Nitro! |
按下「A」鍵即可使用 Nitro! |
|
教學課程 |
圖片:搖桿,用於控制速度/方向 |
圖片:方向鍵和 WASD 鍵,用於控制速度/方向 |
圖片:遊戲手把,用於控制速度/方向 |
追蹤「有效輸入」狀態,然後根據該狀態視需要更新 UI。
注意:請注意,無論遊戲/應用程式處於何種狀態,都應回應所有輸入方式。舉例來說,如果使用者開車時使用觸控螢幕,但按下鍵盤上的 N 鍵,就應觸發氮氣加速動作。
優先狀態變更
部分使用者可能會同時使用觸控螢幕和鍵盤輸入。有些使用者可能會先在平板模式下使用遊戲/應用程式,然後切換到使用鍵盤。其他玩家可能會在遊戲中途連線或中斷連線遊戲控制器。
理想情況下,您不希望出現錯誤的 UI 元素,例如在使用者使用觸控螢幕時,告知他們按下 N 鍵。同時,如果使用者同時使用多個輸入裝置 (例如觸控螢幕和鍵盤),您不希望 UI 在這兩種狀態之間不斷來回切換。
其中一種做法是建立輸入類型優先順序,並在狀態變更之間加入延遲。以上述賽車遊戲為例,您一律要確保在收到螢幕觸控事件時,螢幕上的搖桿會顯示出來,否則使用者可能會覺得遊戲無法操作。這樣觸控螢幕就會成為優先順序最高的裝置。
如果同時收到鍵盤和觸控螢幕事件,遊戲應維持觸控螢幕模式,但仍會對鍵盤輸入做出反應。如果 5 秒後未收到觸控螢幕輸入內容,但仍收到鍵盤事件,螢幕上的控制項可能會淡出,遊戲也會移至鍵盤狀態。
遊戲控制器輸入內容的模式也類似:控制器 UI 狀態的優先順序會低於鍵盤/滑鼠和觸控,且只會在收到遊戲控制器輸入內容 (而非其他形式的輸入內容) 時顯示。遊戲一律會回應控制器輸入內容。
以下是範例的狀態圖和轉換表。根據應用程式或遊戲調整想法。
| #1 觸控螢幕 | #2 鍵盤 | #3 遊戲手把 | |
|---|---|---|---|
|
移至 #1 |
不適用 |
- 收到觸控輸入 |
- 收到觸控輸入 |
|
移至 #2 |
- 5 秒內未觸控 |
不適用 |
- 收到鍵盤輸入內容 |
|
移至 #3 |
- 5 秒內未觸控 |
- No keyboard for 5s |
不適用 |
注意:請注意,優先順序有助於明確指出應以哪種輸入類型為主。輸入狀態會立即「向上」移動優先順序:
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) }