Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

入力イベントの概要

Android では、アプリでのユーザーの操作からイベントをインターセプトする方法が複数用意されています。ユーザー インターフェース内のイベントをインターセプトする場合は、ユーザーが操作する個々の View オブジェクトからイベントを取得することになります。View クラスは、このための手段を提供しています。

レイアウトを構成するために使用する各種の View クラスには、UI イベントの処理に役に立ついくつかのパブリック コールバック メソッドが含まれています。これらのパブリック コールバック メソッドは、オブジェクトで対応するアクションが発生したときに Android フレームワークによって呼び出されます。たとえば、ビュー(ボタンなど)がタップされたときには、そのオブジェクト上で onTouchEvent() メソッドが呼び出されます。このようなイベントをインターセプトするには、クラスを継承しメソッドをオーバーライドする必要があります。ただし、このようなイベントを処理するたびに View オブジェクトを継承することは、実用的とは言えません。そのため、View クラスには、簡単に定義できるコールバックを持つネストされたインターフェースのコレクションも含まれています。これらのインターフェースは、イベントリスナと呼ばれ、UI へのユーザーの操作を取得するために使用されます。

通常は、イベントリスナを使用して、ユーザーの操作をリッスンすることになりますが、カスタム コンポーネントを作成するために View クラスを継承する場合や、処理を工夫するために Button クラスを継承することもできます。その場合は、クラスのイベント ハンドラを使用して、クラスにデフォルトのイベント動作を定義できます。

イベントリスナ

イベントリスナは、コールバック メソッド 1 つを含む View クラスのインターフェースです。以下のメソッドは、イベントリスナが登録されているビューが UI アイテムへのユーザーの操作によってトリガーされたときに、Android フレームワークによって呼び出されます。

イベントリスナ インターフェースに含まれているのは、以下のコールバック メソッドです。

onClick()
View.OnClickListener のメソッド。ユーザーがアイテムをタップしたとき(タッチモードの場合)、またはナビゲーション キーやトラックボールでアイテムにフォーカスを合わせ、適切な「Enter」キーまたはトラックボールを押したときに、呼び出されます。
onLongClick()
View.OnLongClickListener のメソッド。ユーザーがアイテムをタップしてホールドしたとき(タッチモードの場合)、またはナビゲーション キーやトラックボールでアイテムにフォーカスを合わせ、適切な「Enter」キーまたはトラックボールを長押し(1 秒間)したときに、呼び出されます。
onFocusChange()
View.OnFocusChangeListener のメソッド。ナビゲーション キーやトラックボールを使用してユーザーがアイテムに移動したとき、またはアイテムから離れるときに、呼び出されます。
onKey()
View.OnKeyListener のメソッド。ユーザーがアイテムにフォーカスを合わせて端末のハードウェア キーを押したとき、または離したときに、呼び出されます。
onTouch()
View.OnTouchListener のメソッド。画面(アイテムの境界線内)での押す、離す、移動操作などのタップイベントと認定されるアクションをユーザーが実行した場合に呼び出されます。
onCreateContextMenu()
View.OnCreateContextMenuListener のメソッド。コンテキスト メニューが(「長押しクリック」し続けた場合の結果として)ビルドされたときに呼び出されます。コンテキスト メニューについての詳細は、メニューのデベロッパー ガイドをご覧ください。

これらのメソッドは、それぞれのインターフェースにおける唯一のメソッドです。これらのいずれかのメソッドを定義しイベントを処理するには、ネストされたインターフェースをアクティビティに実装するか、ネストされたインターフェースを匿名クラスとして定義します。次に、その実装のインスタンスを対応する View.set...Listener() メソッドに渡します(例: setOnClickListener() を呼び出し、OnClickListener の実装を渡します)。

次の例は、ボタンへの on-click リスナの登録方法を示しています。

Kotlin

protected void onCreate(savedValues: Bundle) {
    ...
    val button: Button = findViewById(R.id.corky)
    // Register the onClick listener with the implementation above
    button.setOnClickListener { view ->
        // do something when the button is clicked
    }
    ...
}

Java

// Create an anonymous implementation of OnClickListener
private OnClickListener corkyListener = new OnClickListener() {
    public void onClick(View v) {
      // do something when the button is clicked
    }
};

protected void onCreate(Bundle savedValues) {
    ...
    // Capture our button from layout
    Button button = (Button)findViewById(R.id.corky);
    // Register the onClick listener with the implementation above
    button.setOnClickListener(corkyListener);
    ...
}

OnClickListener をアクティビティの一部として実装すると、余分なクラスの読み込みとオブジェクトの割り当てを避けることができます。次に例を示します。

Kotlin

class ExampleActivity : Activity(), OnClickListener {
  
    protected fun onCreate(savedValues: Bundle) {
        val button: Button = findViewById(R.id.corky)
        button.setOnClickListener(this)
    }

    // Implement the OnClickListener callback
    fun onClick(v: View) {
        // do something when the button is clicked
    }
}

Java

public class ExampleActivity extends Activity implements OnClickListener {
    protected void onCreate(Bundle savedValues) {
        ...
        Button button = (Button)findViewById(R.id.corky);
        button.setOnClickListener(this);
    }

    // Implement the OnClickListener callback
    public void onClick(View v) {
      // do something when the button is clicked
    }
    ...
}

上の例の onClick() コールバックには戻り値がありませんが、イベントリスナ メソッドの中には、ブール値を返すことが必要なものもあることに注意してください。ブール値が示す事柄はイベントによって異なります。以下はその例です。

  • onLongClick() - イベントを消費済みでこれ以上イベントを引き継ぐべきでないかどうかを示すブール値を返します。つまり、イベントが処理済みでイベントの停止が必要なことを示す場合は、true を返します。イベントが未処理の場合や、他の on-click リスナへのイベントの引き継ぎが必要な場合は、false を返します。
  • onKey() - イベントを消費済みでこれ以上イベントを引き継ぐべきでないかどうかを示すブール値を返します。つまり、イベントが処理済みでイベントの停止が必要なことを示す場合は、true を返します。イベントが未処理の場合や、他の on-key リスナへのイベントの引き継ぎが必要な場合は、false を返します。
  • onTouch() - リスナがイベントを消費しているかどうかを示すブール値を返します。ここで注意する必要があるのは、このイベントが相互にフォローし合う複数のアクションを含むことがあることです。ダウン アクション イベントが受け取られたときに false を返すと、そのイベントを消費しておらず、そのイベントから続けて発生するアクションに関心がないことを示すことになります。そのため、指での操作などのそのイベント内のその他のアクションや操作の最後に発生するアップ アクション イベントで、呼び出されることはなくなります。

ハードウェア キーイベントは常に、その時点でフォーカスがあるビューに届けられることに注意してください。ハードウェア・キーイベントは、ビュー階層の一番上から下に適切な対象に到達するまでディスパッチされます。ビュー(またはビューの子)にフォーカスがある場合、イベントが dispatchKeyEvent() メソッドを介して伝わっていると判断できます。ビューを通じてキーイベントを取得する別の方法としては、onKeyDown()onKeyUp() を使用してアクティビティ内のすべてのイベントを受け取る方法もあります。

また、アプリのテキスト入力について検討する際は、多くの端末にソフトウェア入力メソッドしかないことに注意してください。ソフトウェア入力メソッドでは、キーの利用は必要ありません。音声入力や手書き入力などを利用できるものもあります。入力メソッドがキーボードに似たインターフェースを提供していたとしても、そのインターフェースは通常、onKeyDown() ファミリーのイベントをトリガーしません。アプリの使用をハードウェア キーボード付きの端末に限定するのではない限り、制御するために特定のキーを押すことが必要な UI は作成しないでください。特に、ユーザーのリターンキー押下時に入力を検証するような処理は行わないようにし、代わりに IME_ACTION_DONE などのアクションを使用して入力メソッドにアプリが求める反応を伝えてください。そうすることで、入力メソッドが UI を意味のあるものにします。ソフトウェア入力メソッドの動作を推測し、その推測に基づいて、書式設定済みのテキストをアプリに提供することは避けてください。

注:Android では、まずイベント ハンドラが呼び出され、次にクラス定義から適切なデフォルト ハンドラが呼び出されます。そのため、これらのイベントリスナから true が返された場合、イベントの他のイベントリスナへの伝播は止まり、ビューのデフォルトのイベント ハンドラへのコールバックもブロックされます。そのため、イベントを終了させたいことが確実な場合のみ、true を返すようにしてください。

イベント ハンドラ

ビューからカスタム コンポーネントを作成する場合、デフォルトのイベント ハンドラとして使用される複数のコールバック メソッドを定義できます。イベント ハンドリングに使用される次のような一般的なコールバックの一部については、カスタム コンポーネントについてのドキュメントをご覧ください。

View クラスに含まれていないメソッドの中にも、イベントの処理の方法に直接影響するメソッドがいくつかあります。そのため、レイアウト内の複雑なイベントを管理する場合は、以下のメソッドの使用を検討してください。

タッチモード

ユーザーが方向キーまたはトラックボールを使用してユーザー インターフェースを操作する場合、入力を受け付けるアイテムがどのアイテムなのかユーザーが知ることができるようにアクション可能なアイテム(ボタンなど)にフォーカスを与えることが必要になります。ただし、端末にタップ機能がある場合は、ユーザーはタップでインターフェースの操作を開始するため、アイテムをハイライト表示することや、特定のビューにフォーカスを与えることは必要ではなくなりました。そのため、「タッチモード」と名付けられた操作モードが存在します。

タップ可能な端末では、ユーザーが画面をタップすると、端末がタッチモードになります。端末がタッチモードの場合、テキスト編集ウィジェットなどの isFocusableInTouchMode() が true のビューのみがフォーカス可能であり、ボタンなどのタップ可能なその他のビューは、タップされたときにフォーカスされることはありません。それらのビューは、押されたときに、on-click リスナを起動します。

ユーザーが方向キーを押すか、またはトラックボールでスクロールすると、端末はタッチモードから抜け、ビューがフォーカスを取得します。これにより、ユーザーは、画面をタップすることなく、ユーザー インターフェースの操作を再開できます。

タッチモード状態は、システム全体(すべてのウィンドウとアクティビティ)で維持されます。端末が現在タッチモードかどうか確認するには、isInTouchMode() を呼び出します。

フォーカスの処理

フレームワークは、ユーザー入力に応じて、通常のフォーカス移動を処理します。フレームワークが処理するフォーカス移動には、ビューが削除されたり非表示になったり、新しいビューが利用可能になったりしたことによるフォーカスの変化も含まれます。ビューは、isFocusable() メソッドで、フォーカスが取得可能なことを示します。ビューのフォーカスの取得可否を変更するには、setFocusable() を呼び出します。タッチモードでは、isFocusableInTouchMode() でビューがフォーカスを取得可能かどうか確認できます。ビューのフォーカスの取得可否は、setFocusableInTouchMode() で変更できます。

Android 9(API レベル 28)以上を実行している端末では、アクティビティによって初期フォーカスは割り当てられません。代わりに、必要に応じて明示的に初期フォーカスをリクエストする必要があります。

フォーカスの移動は、所定の方向で最も近くにあるアイテムを見つけるアルゴリズムに基づきますが、まれに、デフォルトのアルゴリズムがデベロッパーの意図する動作と一致しない場合もあります。この場合、レイアウト ファイルの nextFocusDownnextFocusLeftnextFocusRightnextFocusUp の各 XML 属性を明示的にオーバーライドできます。これらの属性のいずれかを、フォーカスの移動元のビューに追加します。フォーカスの移動先のビューの ID になる属性の値を定義します。次に例を示します。

<LinearLayout
    android:orientation="vertical"
    ... >
  <Button android:id="@+id/top"
          android:nextFocusUp="@+id/bottom"
          ... />
  <Button android:id="@+id/bottom"
          android:nextFocusDown="@+id/top"
          ... />
</LinearLayout>

本来、上記の縦方向のレイアウトでは、1 番上のボタンから上に移動しようとしても、2 番目のボタンから下に移動しようしても、どこにもフォーカスが移動しないはずでしたが、上記の処理により、1 番上のボタンによって 1 番下のボタンが nextFocusUp として定義され(逆の場合も同様)、フォーカスが、上から下と下から上に循環するようになります。

UI でビューをフォーカス可能にする場合は(従来は、ビューはフォーカス可能ではありません)、レイアウトの宣言で android:focusable XML 属性をビューに追加します。値 true を設定します。また、android:focusableInTouchMode を使用して、タッチモードのときにビューをフォーカス可能にすることもできます。

特定のビューへのフォーカスをリクエストするには、requestFocus() を呼び出します。

フォーカス イベントをリッスンするには(ビューがフォーカスを受け取ったときまたは失ったときに通知を受け取るには)、上記のイベントリスナのセクションの説明に従って onFocusChange() を使用します。