入力の変更を処理する

Chromebook では、キーボード、マウス、トラックパッド、タッチスクリーン、タッチペン、MIDI、ゲームパッド/ブルー コントローラなど、さまざまな入力オプションを利用できます。つまり、同じデバイスが DJ のステーション、アーティストのキャンバス、AAA ストリーミング ゲームのゲーマー向けプラットフォームになる可能性があります。

デベロッパーは、ユーザーがすでに持っている入力デバイス(接続されたキーボード、スタイラス、Stadia ゲーム コントローラなど)を活用して、ユーザーに多用途でエキサイティングなアプリ エクスペリエンスを提供できます。ただし、これらの可能性をすべて考慮して、アプリのエクスペリエンスをスムーズかつ論理的なものにするために、UI についても検討する必要があります。アプリやゲームがスマートフォンを念頭に置いて設計されている場合は、特にこの点が重要になります。たとえば、ゲームにスマートフォン用の画面上のタッチ操作式ジョイスティックがある場合、ユーザーがキーボードでプレイしているときは、このジョイスティックを非表示にしたいでしょう。

このページでは、複数の入力ソースを検討する際に注意すべき主な問題と、それに対処するための戦略について説明します。

サポートされている入力方法のユーザーによる検出

理想的には、アプリはユーザーが選択した入力にシームレスに対応します。多くの場合、これは簡単で、ユーザーに追加の情報を提供する必要はありません。たとえば、ユーザーがマウス、トラックパッド、タッチスクリーン、スタイラスなどでボタンをクリックした場合、ボタンは機能する必要があります。また、ユーザーにこれらのさまざまなデバイスを使用してボタンを有効にできることを伝える必要はありません。

ただし、入力方法によってユーザー エクスペリエンスが向上する状況もあり、アプリがその入力方法をサポートしていることをユーザーに知らせるのが妥当な場合もあります。例:

  • メディア再生アプリは、ユーザーが簡単に推測できない便利なキーボード ショートカットを多数サポートしている場合があります。
  • DJ アプリを作成した場合、ユーザーは最初はタッチスクリーンを使用する可能性があり、キーボードやトラックパッドを使用して一部の機能に触覚的にアクセスできることを認識していない可能性があります。同様に、MIDI DJ コントローラを多数サポートしていることに気づいていない可能性もあります。サポートされているハードウェアを確認するよう促すことで、より本格的な DJ プレイを楽しめるかもしれません。
  • タッチスクリーンとキーボード/マウスで快適にプレイできるゲームでも、Bluetooth ゲーム コントローラを多数サポートしていることにユーザーが気づいていない可能性があります。これらのいずれかを接続すると、ユーザーの満足度とエンゲージメントを高めることができます。

適切なタイミングでメッセージを表示することで、ユーザーが入力オプションを見つけられるようにすることができます。実装はアプリごとに異なります。例をいくつか示します。

  • 初回実行時またはヒントのポップアップ
  • 設定パネルの構成オプションは、サポートが存在することをユーザーに受動的に示すことができます。たとえば、ゲームの設定パネルに [ゲーム コントローラ] タブが表示されていれば、コントローラがサポートされていることを示します。
  • コンテキスト メッセージ。たとえば、物理キーボードを検出し、ユーザーがマウスまたはタッチスクリーンを使用してアクションをクリックしていることがわかった場合は、キーボード ショートカットを提案するヒントを表示するとよいでしょう。
  • キーボード ショートカットのリスト。物理キーボードが検出されたときに、利用可能なキーボード ショートカットの一覧を表示する方法を UI で示すことで、キーボード サポートがあることを宣伝するとともに、サポートされているショートカットをユーザーが簡単に確認して記憶できるようにする、という 2 つの目的を達成できます。

入力バリエーションの UI ラベル付け/レイアウト

理想的には、別の入力デバイスが使用されても、ビジュアル インターフェースを大幅に変更する必要はなく、可能なすべての入力が「そのまま機能する」はずです。ただし、重要な例外があります。主なものとしては、タッチ専用の UI と画面上のプロンプトがあります。

タッチ専用 UI

アプリにタッチ専用の UI 要素(ゲームの画面上のジョイスティックなど)がある場合は、タッチを使用していないときのユーザー エクスペリエンスを考慮する必要があります。一部のモバイルゲームでは、タッチ操作に必要なコントロールが画面の大部分を占めていますが、ユーザーがゲームパッドやキーボードを使ってプレイする場合は、これらのコントロールは不要です。アプリやゲームは、現在使用されている入力方法を検出し、それに応じて UI を調整するロジックを提供する必要があります。これを行う方法の例については、下記の実装のセクションをご覧ください。

カーレース ゲームの UI - 画面上のコントロールとキーボードのコントロール

画面上のプロンプト

アプリで、ユーザーに役立つ画面上のプロンプトが表示されている可能性があります。これらが入力デバイスに依存しないように注意してください。次に例を示します。

  • スワイプして…
  • 閉じるには画面をタップします
  • ピンチしてズーム
  • [X] を押すと…
  • 長押しして有効にする

一部のアプリでは、入力に依存しないように文言を調整できる場合があります。これは、意味がある場合は望ましいですが、多くの場合、具体性が重要であり、使用されている入力方法に応じて異なるメッセージを表示する必要がある場合があります。特に、アプリの初回実行時などのチュートリアル モードでは、その傾向が顕著です。

多言語に関する考慮事項

アプリが複数の言語に対応している場合は、文字列アーキテクチャを検討する必要があります。たとえば、3 つの入力モードと 5 つの言語をサポートしている場合、すべての UI メッセージに 15 種類のバージョンが存在する可能性があります。これにより、新機能の追加に必要な作業量が増え、スペルミスが発生する可能性が高くなります。

1 つの方法は、インターフェース アクションを別個の文字列セットとして考えることです。たとえば、「閉じる」アクションを「タップして閉じる」、「Esc キーを押して閉じる」、「クリックして閉じる」、「ボタンを押して閉じる」などの入力固有のバリエーションを含む独自の文字列変数として定義すると、ユーザーに何かを閉じる方法を伝える必要があるすべての UI メッセージで、この単一の「閉じる」文字列変数を使用できます。入力メソッドが変更されたら、この 1 つの変数の値を変更するだけです。

ソフト キーボード / IME 入力

ユーザーが物理キーボードなしでアプリを使用している場合、テキスト入力は画面キーボードで行われることを覚えておいてください。画面キーボードが表示されたときに必要な UI 要素が隠れないことを必ずテストしてください。詳しくは、Android IME の表示に関するドキュメントをご覧ください。

実装

ほとんどの場合、アプリやゲームは、画面に表示されている内容に関係なく、すべての有効な入力に正しく応答する必要があります。ユーザーが 10 分間タッチスクリーンを使用した後、突然キーボードの使用に切り替えた場合、画面上の視覚的なプロンプトやコントロールに関係なく、キーボード入力が機能する必要があります。つまり、機能はビジュアルやテキストよりも優先されるべきです。これにより、入力検出ロジックにエラーがある場合や予期しない状況が発生した場合でも、アプリやゲームが使用可能であることが保証されます。

次のステップでは、現在使用されている入力方法の正しい UI を表示します。どのようにして正確に検出するのでしょうか?アプリの使用中にユーザーが入力方法を切り替えた場合はどうなりますか?複数の方法を同時に使用している場合はどうなりますか?

受信したイベントに基づく優先順位付けされたステート マシン

1 つの方法は、アプリが受け取った実際の入力イベントを追跡し、優先順位付けされたロジックを使用して状態を切り替えることで、現在画面に表示されている入力プロンプトを表す現在の「アクティブな入力状態」を追跡することです。

優先順位付け

入力状態を優先する理由ユーザーはさまざまな方法でデバイスを操作するため、アプリはユーザーの選択をサポートする必要があります。たとえば、ユーザーがタッチスクリーンと Bluetooth マウスを同時に使用することを選択した場合、その操作はサポートされるべきです。しかし、どの画面上の入力プロンプトとコントロールを表示すべきでしょうか?マウスまたはタッチ?

各プロンプトのセットを「入力状態」として定義し、優先順位を付けることで、一貫した方法で決定できます。

受信した入力イベントによって状態が決まる

受信した入力イベントのみを処理するのはなぜですか?Bluetooth 接続を追跡して Bluetooth コントローラが接続されているかどうかを判断したり、USB デバイスの USB 接続を監視したりできるとお考えかもしれません。この方法は、主に次の 2 つの理由からおすすめできません。

まず、API 変数に基づいて接続されたデバイスについて推測できる情報は一貫性がなく、Bluetooth/ハードウェア/スタイラス デバイスの数は増え続けています。

接続ステータスではなく受信したイベントを使用する 2 つ目の理由は、ユーザーがマウス、Bluetooth コントローラ、MIDI コントローラなどを接続していても、アプリやゲームの操作に積極的に使用していない可能性があるためです。

アプリがアクティブに受信した入力イベントに応答することで、不完全な情報からユーザーの意図を推測するのではなく、ユーザーの実際の操作にリアルタイムで応答できます。

例: タッチ、キーボード/マウス、コントローラに対応したゲーム

タッチベースの携帯電話向けにカーレース ゲームを開発したとします。プレイヤーは、加速、減速、右折、左折、ニトロを使用してスピードを上げることができます。

現在のタッチスクリーン インターフェースは、画面の左下に速度と方向を制御する画面上のジョイスティック、右下にニトロ用のボタンで構成されています。ユーザーはトラック上でニトロ カンを収集できます。画面下部のニトロ バーが満タンになると、ボタンの上に「ニトロ発動!」というメッセージが表示されます。初めてゲームをプレイする場合や、しばらく入力がない場合は、ジョイスティックの上に「チュートリアル」のテキストが表示され、車の動かし方が説明されます。

キーボードと Bluetooth ゲーム コントローラのサポートを追加したい。どこから始めればよいでしょうか?

タッチ操作のカーレース ゲーム

入力状態

まず、ゲームが実行される可能性のあるすべての入力状態を特定し、各状態で変更するパラメータをすべてリストアップします。

                                                                                                                                                                        
タップキーボード/マウスゲーム用コントローラ
       

対応先

     
       

すべての入力

     
       

すべての入力

     
       

すべての入力

     
       

画面上の操作

     
       

- 画面上のジョイスティック
- ニトロボタン

     
       

- ジョイスティックなし
- ニトロボタンなし

     
       

- ジョイスティックなし
- ニトロボタンなし

     
       

テキスト

     
       

タップして Nitro を入手しよう!

     
       

Nitro を利用するには「N」を押してください。

     
       

Nitro を利用するには「A」を押してください。

     
       

チュートリアル

     
       

速度/方向のジョイスティックの画像

     
       

速度と方向を示す矢印キーと WASD の画像

     
       

速度/方向のゲームパッドの画像

     

「アクティブな入力」の状態を追跡し、その状態に基づいて必要に応じて UI を更新します。

注: 状態にかかわらず、ゲーム/アプリはすべての入力方法に対応する必要があります。たとえば、ユーザーがタッチスクリーンで車を運転しているときに、キーボードの N を押すと、ニトロ アクションがトリガーされる必要があります。

優先順位付けされた状態変更

一部のユーザーは、タッチスクリーンとキーボード入力を同時に使用する可能性があります。タブレット モードでソファに座ってゲームやアプリを使い始め、その後、テーブルでキーボードを使って操作するようになるユーザーもいます。ゲーム中にゲーム コントローラが接続または切断される場合もあります。

理想的には、タッチスクリーンを使用しているユーザーに n キーを押すよう指示するなど、誤った UI 要素は表示しないようにします。同時に、タッチスクリーンやキーボードなど複数の入力デバイスを同時に使用しているユーザーの場合、UI が 2 つの状態を絶えず切り替えるのは望ましくありません。

この問題を処理する方法の 1 つは、入力タイプの優先度を設定し、状態の変化の間に遅延を組み込むことです。上記のカーゲームでは、画面タッチ イベントが受信されるたびに画面上のジョイスティックが表示されるようにする必要があります。そうしないと、ユーザーがゲームを使用できないように見える可能性があります。これにより、タッチスクリーンが最優先のデバイスになります。

キーボードとタッチスクリーンのイベントが同時に受信された場合、ゲームはタッチスクリーン モードのままになりますが、キーボード入力には反応します。5 秒間タッチスクリーン入力がなく、キーボード イベントがまだ受信されている場合は、画面上のコントロールがフェードアウトし、ゲームがキーボード状態に移行する可能性があります。

ゲーム コントローラの入力も同様のパターンになります。コントローラの UI 状態はキーボード/マウスやタッチよりも優先度が低く、ゲーム コントローラの入力が受信され、他の形式の入力が受信されていない場合にのみ表示されます。ゲームは常にコントローラの入力に応答します。

以下に、この例の状態図と遷移表を示します。アイデアをアプリやゲームに合わせます。

優先順位付きステート マシン - タッチスクリーン、キーボード/マウス、ゲーム コントローラ

                                                                                                                                        
#1 タッチスクリーン#2 キーボード#3 ゲームパッド
       

#1 に移動

     
       

なし

     
       

- タップ入力あり
- タップ入力状態にすぐに移行

     
       

- タップ入力あり
- タップ入力状態にすぐに移行

     
       

#2 に移動

     
       

- 5 秒間タッチなし
- キーボード入力あり
- キーボード入力状態に移行

     
       

なし

     
       

- キーボード入力の受信
(すぐにキーボード入力状態に移行)

     
       

#3 に移動

     
       

- 5 秒間タッチなし
- 5 秒間キーボードなし
- ゲームパッド入力を受信
- ゲームパッド入力状態に移行

     
       

- 5 秒間キーボード操作なし
- ゲームパッド入力あり
- ゲームパッド入力状態に移行

     
       

なし

     

注: 優先順位付けによって、どのタイプの入力が優先されるかが明確になります。入力状態の優先度が即座に「上がる」:

3. Gamepad -> 2. キーボード -> 1. タッチ

優先度の高いデバイスが使用されるとすぐに切り替わりますが、優先度は遅延期間の経過後、優先度の低いデバイスがアクティブに使用されている場合にのみ、ゆっくりと「下」に移動します。

入力イベント

標準の 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() はキープレスごとに 1 回のみトリガーされますが、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)
}