大画面での入力の互換性

大画面のデバイスでは、キーボード、マウス、トラックパッド、タッチペン、ゲームパッドを使用してアプリを操作することが多くなります。アプリが外部デバイスからの入力を受け入れるようにするには、以下を行います。

  • 基本的なキーボード サポートをテストする (元に戻すの Ctrl+Z、コピーの Ctrl+C、保存の Ctrl+S など)。デフォルトのキーボード ショートカットの一覧については、 キーボード アクションの処理をご覧ください。
  • 高度なキーボード サポートをテストするTab キーと 矢印キーによるキーボード ナビゲーション、Enter キーによるテキスト入力の確定、 メディアアプリでの Spacebar による再生と一時停止など)。
  • 基本的なマウス操作をテストする(右クリックしてコンテキスト メニューを表示する、 ユーザーがカーソルを合わせたときにアイコンを変更する、マウスホイールまたはトラックパッドでカスタム コンポーネントをスクロールするなど)。
  • アプリ固有の入力デバイスをテストする (タッチペン、ゲーム コントローラ、音楽アプリの MIDI コントローラなど)。
  • 高度な入力のサポートを検討する (パソコン環境でアプリの魅力を高めたい場合。DJ アプリ用のクロスフェーダーとしてのタッチパッド、ゲーム用のマウス キャプチャ、キーボードを多用するユーザー向けのキーボード ショートカットなど)。

キーボード

キーボード入力に対してアプリがどのように応答するかは、大画面でのユーザー エクスペリエンスを高める鍵になります。キーボード入力には、ナビゲーションキーストロークショートカットの 3 種類があります。

タップ中心のアプリにキーボード ナビゲーションが実装されることはまれですが、ユーザーはアプリの使用時にキーボードがあれば、キーボード ナビゲーションを期待します。ユーザー補助機能を必要とするユーザーにとっては、スマートフォン、タブレット、折りたたみ式デバイス、パソコンでキーボード ナビゲーションが不可欠となる場合があります。

多くのアプリでは、矢印キーと Tab キーによるナビゲーションは Android フレームワークによって 自動的に処理されます。たとえば、一部のコンポーザブルは デフォルトでフォーカス可能であり、Buttonclickable 修飾子を持つコンポーザブルなど、キーボード ナビゲーションは一般的に追加の コードなしで機能します。デフォルトでは フォーカス可能でないカスタム コンポーザブルのキーボード ナビゲーションを有効にするには、focusable 修飾子を追加します。

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

詳しくは、コンポーザブルをフォーカス可能にするをご覧ください。

フォーカスが有効になると、Android フレームワークはすべてのフォーカス可能なコンポーネントのナビゲーション マッピングを、位置に基づいて作成します。通常、これは想定どおりに機能し、特別な開発は不要です。

ただし、Compose では、タブやリストなどの複雑なコンポーザブルのタブ ナビゲーションで、常に正しい次のアイテムが決定されるとは限りません。たとえば、コンポーザブルの 1 つが完全に表示されていない水平スクロール可能な場合などです。

フォーカスの動作を制御するには、focusGroup 修飾子をコンポーザブルのコレクションの親 コンポーザブルに追加します。フォーカスはグループに移動し、グループ内を移動してから、次のフォーカス可能なコンポーネントに移動します。例:

Row {
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col1") }
        Button({}) { Text("Row2 Col1") }
        Button({}) { Text("Row3 Col1") }
    }
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col2") }
        Button({}) { Text("Row2 Col2") }
        Button({}) { Text("Row3 Col2") }
    }
}

詳しくは、フォーカス グループを使用して一貫したナビゲーションを提供するをご覧ください。

キーボードのみを使用して、アプリのすべての UI 要素にアクセスできることをテストします。よく使用する要素には、マウス入力やタップ入力なしでアクセスできるようにする必要があります。

ユーザー補助機能を必要とするユーザーにとっては、キーボード サポートが不可欠であることに留意してください。

キーストローク

画面上の仮想キーボード(IME)によって処理されるテキスト入力の場合、 TextField のようなアプリは、追加の開発作業がなくても大画面デバイスで想定どおりに動作します。フレームワークが予測できないキーストロークについては、アプリが独自に動作を処理する必要があります。これは、特にカスタムビューを使用するアプリに当てはまります。

例としては、Enter キーを使用してメッセージを送信するチャットアプリ、 Spacebar で再生の開始と停止を行うメディアアプリ、および was、および d キーで移動を制御するゲームなどがあります。

個々のキーストロークは onKeyEvent 修飾子で処理できます。この修飾子は、変更されたコンポーネントがキーイベントを受け取ったときに呼び出されるラムダを受け取ります。KeyEvent#type プロパティを使用すると、イベントが キーの押下(KeyDown)かキーの解放(KeyUp)かを判断できます。

Box(
    modifier = Modifier.focusable().onKeyEvent {
        if(
            it.type == KeyEventType.KeyUp &&
            it.key == Key.S
        ) {
            doSomething()
            true
        } else {
            false
        }
    }
)  {
    Text("Press S key")
}

または、onKeyUp() コールバックをオーバーライドし、 受け取ったキーコードごとに期待される動作を追加することもできます。

kotlin override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { return when (keyCode) { KeyEvent.KEYCODE_ENTER -> { sendChatMessage() true } KeyEvent.KEYCODE_SPACE -> { playOrPauseMedia() true } else -> super.onKeyUp(keyCode, event) } }

An onKeyUp イベントは、キーが離されたときに発生します。このコールバックを使用すると、キーを長押ししたりゆっくり離したりした場合に、アプリが複数の onKeyDown イベントを処理する必要がなくなります。キーが押された瞬間を検出する必要があるか、ユーザーがキーを長押ししているかどうかを検出する必要があるゲームやアプリは、onKeyDown イベントをリッスンし、繰り返される onKeyDown イベントを独自に処理できます。

詳しくは、キーボード アクションの処理をご覧ください。

ショートカット

ハードウェア キーボードを使用する際は、Ctrl キー、Alt キー、 Shift キー、Meta キーを含む一般的なキーボード ショートカットが求められます。こうしたショートカットがアプリに実装されていないと、ユーザーがフラストレーションを感じる可能性があります。また上級ユーザーにとっては、よく使うアプリ固有のタスクのショートカットが役立ちます。ショートカットを実装することで、アプリの使いやすさを高め、ショートカットを備えていないアプリとの差別化を図ることができます。

一般的なショートカットには、Ctrl+S(保存)、Ctrl+Z (元に戻す)、Ctrl+Shift+Z(やり直し)などがあります。デフォルトのショートカットの一覧については、 キーボード アクションの処理をご覧ください。

KeyEvent オブジェクトには、 修飾キーが押されているかどうかを示す次の属性があります。

次に例を示します。

Box(
    Modifier.onKeyEvent {
        if (it.isAltPressed && it.key == Key.A) {
            println("Alt + A is pressed")
            true
        } else {
            false
        }
    }
    .focusable()
)

詳しくは以下をご覧ください。

タッチペン

多くの大画面デバイスにはタッチペンが付属しています。Android アプリはタッチペンをタッチスクリーン入力として処理します。Wacom Intuos のように、USB または Bluetooth の描画 テーブルを備えたデバイスもあります。Android アプリは Bluetooth 入力は受け取れますが、USB 入力は受け取れません。

タッチペンの MotionEvent オブジェクトにアクセスするには、描画サーフェスに pointerInteropFilter 修飾子を追加します。モーション イベントを処理するメソッドを使用して ViewModel クラスを実装し、そのメソッドを onTouchEvent ラムダとして pointerInteropFilter 修飾子として渡します。

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
   Canvas(modifier = modifier
       .clipToBounds()
       .pointerInteropFilter {
           viewModel.processMotionEvent(it)
       }

   ) {
       // Drawing code here.
   }
}

MotionEvent オブジェクトには、イベントに関する情報が格納されます。

履歴ポイント

Android は、入力イベントを一括でまとめてフレームごとに 1 回送信します。タッチペン入力では、ディスプレイよりもはるかに高い頻度でイベントが報告される可能性があります。描画アプリを作成する場合は、getHistorical API を使用して、最近発生した可能性があるイベントをチェックします。

パーム リジェクション

ユーザーがタッチペンを使用して描画、書き込み、またはアプリの操作を行うと、手のひらが画面に触れることがあります。タッチイベント( ACTION_DOWN または ACTION_POINTER_DOWN に設定)が、システムに認識される前にアプリに報告される場合、手のひらでの意図しないタッチは無視されます。

Android は、MotionEvent をディスパッチすることにより、手のひらでのタッチイベントをキャンセルします。アプリが ACTION_CANCELを受け取った場合、操作はキャンセルされます。アプリが ACTION_POINTER_UPを受け取った場合は、FLAG_CANCELEDが設定されているかどうかを確認します。設定されている場合は、操作をキャンセルします。

FLAG_CANCELED のみの確認はしないでください。Android 13(API レベル 33)以降では、ACTION_CANCEL イベントに FLAG_CANCELED が設定されますが、Android の古いバージョンでは設定されません。

Android 12

Android 12(API レベル 32)以前の場合、パーム リジェクションの検出は、シングル ポインタ タッチイベントでのみ可能です。手のひらでのタッチが唯一のポインタである場合は、モーション イベント オブジェクトに ACTION_CANCEL が設定され、イベントはキャンセルされます。他のポインタがダウンしている場合、システムは ACTION_POINTER_UP を設定しますが、パーム リジェクション検出には不十分です。

Android 13

Android 13(API レベル 33)以降では、手のひらでのタッチが唯一のポインタである場合、モーション イベント オブジェクトに ACTION_CANCELFLAG_CANCELED が設定され、イベントがキャンセルされます。他のポインタがダウンしている場合、ACTION_POINTER_UPFLAG_CANCELED が設定されます。

アプリが ACTION_POINTER_UP でモーション イベントを受け取った場合は必ず FLAG_CANCELED をチェックして、イベントがパーム リジェクション(または他のイベントのキャンセル)を示しているかどうかを確認します。

メモ作成アプリ

ChromeOS には、登録されたメモ作成アプリをユーザーに表示するための特別なインテントがあります。アプリをメモ作成アプリとして登録するには、アプリ マニフェストに次の行を追加します。

<intent-filter>
    <action android:name="org.chromium.arc.intent.action.CREATE_NOTE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

アプリがシステムに登録されると、ユーザーはそのアプリをデフォルトのメモ作成アプリとして選択できます。新しいメモがリクエストされた場合、アプリはタッチペン入力が可能な空のメモを作成する必要があります。ユーザーが画像(スクリーンショットやダウンロードした画像など)にアノテーションを付けようとした場合、アプリは content:// URI を持つアイテムを 1 つ以上含む ClipData とともに起動します。アプリは、最初の添付画像を背景画像として使用するメモを作成し、ユーザーがタッチペンで画面に描画できるモードに移行する必要があります。

タッチペンなしでメモ作成インテントをテストする

アプリがメモ作成インテントに正しく応答するかどうかを、アクティブなタッチペンがない状態でテストするには、次の方法で ChromeOS のメモ作成オプションを表示します。

  1. デベロッパー モードに切り替えて、デバイスを書き込み可能にする
  2. Ctrl+Alt+F2 キーを押して、ターミナルを開く
  3. コマンド sudo vi /etc/chrome_dev.conf を実行する
  4. i を押して入力モードに切り替え、ファイルの末尾に新しい行として --ash-enable-palette を追加する
  5. Esc キーを押してから :wq を入力し、Enter キーを押して保存する
  6. Ctrl+Alt+F1 キーを押して、通常の ChromeOS UI に戻る
  7. ログアウトしてからログインし直す

これで、シェルフにタッチペン メニューが表示されるようになります。

  • シェルフ内のタッチペン ボタンをタップし、[新しいメモ] を選択します。これにより、空白の描画メモが開きます。
  • スクリーンショットを撮ります。シェルフからタッチペン ボタン > [画面をキャプチャ] を選択するか、画像をダウンロードします。画像にアノテーションを付ける オプションが通知に表示されます。これにより、画像にアノテーションを付けられる状態でアプリが起動します。

マウスとタッチパッドのサポート

一般的にほとんどのアプリは、右クリック、マウスオーバー、ドラッグ&ドロップという 3 つの大画面中心のイベントを処理するだけで済みます。 右クリックマウスオーバードラッグ& ドロップ

右クリック

リストアイテムの長押しなど、アプリにコンテキスト メニューを表示させるためのアクションはすべて、右クリック イベントにも反応する必要があります。

右クリック イベントを処理するには、アプリで View.OnContextClickListenerを登録する必要があります。

Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            val rootView = FrameLayout(context)
            val onContextClickListener =
                View.OnContextClickListener { view ->
                    showContextMenu()
                    true
                }
            rootView.setOnContextClickListener(onContextClickListener)
            rootView
        },
    )
}

コンテキスト メニューの作成の詳細については、コンテキスト メニューを作成するをご覧ください。

カーソルを合わせる

マウスオーバー イベントを適切に処理することで、アプリのレイアウトが洗練されていて使いやすいとユーザーに感じさせることができます。これは、カスタム コンポーネントに特に当てはまります。

最も一般的な例は次の 2 つです。

  • マウスポインタ アイコンを変更することで、要素にインタラクティブな動作がある(たとえば、クリック可能または編集可能である)ことをユーザーに知らせる
  • 大規模なリストまたはグリッド内のアイテムにポインタがカーソルを合わせたとき、そのアイテムに視覚的なフィードバックを追加する

ドラッグ&ドロップ

マルチウィンドウ環境では、ユーザーはアプリ間でアイテムをドラッグ&ドロップできることを期待します。これは、デスクトップ デバイス、タブレット、スマートフォン、分割画面モードの折りたたみ式デバイスに当てはまります。

ユーザーがアプリにアイテムをドラッグする可能性があるかどうかを考慮します。たとえば、写真エディタによる写真の受け取り、オーディオ プレーヤーによる音声ファイルの受け取り、描画プログラムによる写真の受け取りなどがあります。

ドラッグ&ドロップのサポートを追加するには、 ドラッグ&ドロップ をご覧ください。Android on ChromeOS — Implementing Drag & Drop のブログ投稿もご覧ください。

ChromeOS 向けの特別な考慮事項

高度なポインタのサポート

マウス入力とタッチパッド入力の高度な処理を行うアプリは、 pointerInput 修飾子を実装して PointerEvent を取得する必要があります。

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

PointerEvent オブジェクトを調べて、次のことを確認します。

ゲーム コントローラ

大画面の Android デバイスの中には、最大 4 つのゲーム コントローラをサポートするものもあります。標準の Android ゲーム コントローラ API を使用してゲーム コントローラを処理します(ゲーム コントローラをサポートするをご覧ください)。

ゲーム コントローラのボタンは、一般的なマッピングに従って一般的な値にマッピングされます。 ただし、すべてのゲーム コントローラ メーカーが同じマッピング規則に従っているわけではありません。 各種の一般的なコントローラ マッピングをユーザーが選択できるようにすると、エクスペリエンスを大幅に向上させることができます。詳しくは、ゲームパッド ボタンの押下を処理するをご覧ください。

入力変換モード

ChromeOS では、デフォルトで入力変換モードが有効になっています。ほとんどの Android アプリの場合、このモードはパソコン環境で想定されるとおりに動作します。たとえば、タッチパッドでの 2 本指スクロール、マウスホイールによるスクロール、未加工のディスプレイ座標のウィンドウ座標へのマッピングなどが自動的に有効になります。 通常、アプリ デベロッパーはこうした動作を実装する必要はありません。

カスタムの入力動作をアプリに実装する(たとえば 2 本の指でタッチパッドをつまむカスタム操作を定義する)場合や、アプリが想定する入力イベントが入力変換によって提供されない場合は、Android マニフェストに次のタグを追加することにより、入力変換モードを無効にできます。

<uses-feature
    android:name="android.hardware.type.pc"
    android:required="false" />

参考情報