高度なタッチペン機能をサポートする

タッチペンにより、アプリを快適に操作でき、正確なピンポイントでメモを取ったり、スケッチしたり、生産性向上アプリを使用したり、ゲームアプリやエンタメアプリをリラックスして楽しむことができます。

Android と ChromeOS では、卓越したタッチペン エクスペリエンスをアプリで可能にするためのさまざまな API を使用できます。MotionEvent クラスは、タッチペンの圧力、向き、傾斜、ホバー、手のひら検出など、画面でのユーザー操作に関する情報を提供します。低レイテンシのグラフィックスとモーション予測ライブラリにより、画面上のタッチペンのレンダリングが強化され、ペンと紙のように自然に操作できます。

MotionEvent

MotionEvent クラスは、画面上のタッチポインタの位置や移動など、ユーザー入力操作を表します。タッチペン入力の場合、MotionEvent は圧力、向き、傾斜、ホバーのデータも表します。

イベントデータ

ビューベースのアプリで MotionEvent データにアクセスするには、onTouchListener を設定します。

Kotlin

val onTouchListener = View.OnTouchListener { view, event ->
  // Process motion event.
}

Java

View.OnTouchListener listener = (view, event) -> {
  // Process motion event.
};

リスナーは MotionEvent オブジェクトをシステムから受け取り、アプリで処理できるようにします。

MotionEvent オブジェクトは、UI イベントの次の要素に関連するデータを提供します。

  • アクション: デバイスの物理的な操作 - 画面をタップする、画面上でポインタを移動する、画面上でホバーする
  • ポインタ: 画面を操作するオブジェクトの識別子(指、タッチペン、マウス)
  • 軸: データの種類 - x 座標、y 座標、圧力、傾斜、方向、ホバー(距離)

アクション

タッチペンのサポートを実装するには、ユーザーが実行しているアクションを把握する必要があります。

MotionEvent には、モーション イベントを定義するさまざまな ACTION 定数が用意されています。タッチペンで最も重要なアクションには次のようなものがあります。

アクション 説明
ACTION_DOWN
ACTION_POINTER_DOWN
ポインタが画面と接触しました。
ACTION_MOVE 画面上でポインタが動いています。
ACTION_UP
ACTION_POINTER_UP
ポインタが画面に接触しなくなりました。
ACTION_CANCEL 前回または現在のモーション セットをキャンセルする必要があるとき。

アプリは、ACTION_DOWN が発生したときに新しいストロークを開始する、ACTION_MOVE, でストロークを描画する、ACTION_UP がトリガーされたときにストロークを終了するなどのタスクを実行できます。

特定のポインタについての ACTION_DOWN から ACTION_UP までの MotionEvent アクションのセットを、モーション セットと呼びます。

ポインタ

ほとんどの画面はマルチタッチです。システムは、画面を操作するそれぞれの指、タッチペン、マウスなどのポインティング オブジェクトにポインタを割り当てます。ポインタ インデックスを使用すると、1 本目の指の画面に接触する指や 2 本目の指の位置など、特定のポインタの軸情報を取得できます。

ポインタ インデックスの範囲は、最小値はゼロ、最大値は MotionEvent#pointerCount() によって返されるポインタの数から 1 を引いた値になります。

ポインタの軸の値にアクセスするには、getAxisValue(axis, pointerIndex) メソッドを使用します。ポインタ インデックスを省略すると、最初のポインタであるポインタ 0 の値が返されます。

MotionEvent オブジェクトには、使用中のポインタの種類に関する情報が含まれています。ポインタの種類を取得するには、ポインタ インデックスに対して反復処理を行い、getToolType(pointerIndex) メソッドを呼び出します。

ポインタについて詳しくは、マルチタッチ ジェスチャーの処理をご覧ください。

タッチペン入力

TOOL_TYPE_STYLUS を使用すると、タッチペン入力をフィルタできます。

Kotlin

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

Java

boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);

TOOL_TYPE_ERASER を使用すると、タッチペンが消しゴムとして使用されていることも報告できます。

Kotlin

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

Java

boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);

タッチペンの軸データ

ACTION_DOWNACTION_MOVE は、タッチペンに関する軸データ、すなわち x 座標と y 座標、圧力、向き、傾斜、ホバーのデータを提供します。

このデータへのアクセスのため、MotionEvent API には getAxisValue(int) が用意されています。ここで、パラメータは次のいずれかの軸 ID です。

getAxisValue() の戻り値
AXIS_X モーション イベントの x 座標。
AXIS_Y モーション イベントの y 座標。
AXIS_PRESSURE タッチスクリーンまたはタッチパッドの場合は、指やタッチペンなどのポインタでかかる圧力。マウスまたはトラックボールの場合は、メインボタンが押された場合は 1、それ以外の場合は 0 になります。
AXIS_ORIENTATION タッチスクリーンやタッチパッドの場合は、デバイスの垂直面を基準とした指やタッチペンなどのポインタの向き。
AXIS_TILT タッチペンの傾斜角度(ラジアン単位)。
AXIS_DISTANCE 画面からタッチペンまでの距離。

たとえば MotionEvent.getAxisValue(AXIS_X) は、最初のポインタの x 座標を返します。

マルチタッチ ジェスチャーの処理もご覧ください。

位置

次の呼び出しを使用して、ポインタの x 座標と y 座標を取得できます。

x 座標と y 座標がマッピングされた、画面上のタッチペン描画。
図 1. タッチペン ポインタの x 画面座標と y 画面座標。

圧力

次の呼び出しを使用して、ポインタの圧力を取得できます。

最初のポインタの場合は getAxisValue(AXIS_PRESSURE) または getPressure()

タッチスクリーンまたはタッチパッドの圧力値は 0(圧力なし)~1 の範囲ですが、画面の調整によっては高い値が返される場合があります。

低圧から高圧への連続的な圧力を表すタッチペンのストローク。ストロークは左側では細く薄くなっていて、圧力が低いことを表します。ストロークは、左から右に行くほど太く濃くなります。右端では最も太く最も濃くなっていて、圧力が最も高いことを表します。
図 2. 圧力 - 左側は低圧、右側は高圧

向き

方向とは、タッチペンが向いている方向を示します。

ポインタの向きは、getAxisValue(AXIS_ORIENTATION) または getOrientation()(最初のポインタ)を使用して取得できます。

タッチペンの場合、向きは 0~π(時計回り)または 0~-π(反時計回り)のラジアン値として返されます。

向きを使用すると、実際のブラシを実装できます。たとえば、タッチペンがフラットブラシを表す場合、フラットブラシの幅はタッチペンの向きによって異なります。

図 3. 左(マイナス 0.57 ラジアン)を示すタッチペン。

傾斜

傾斜は、画面に対するタッチペンの傾きを測定したものです。

傾斜として、タッチペンの正の角度がラジアン単位で返されます。ゼロは画面に対して垂直、π/2 は画面上で水平であることを表します。

傾斜角は getAxisValue(AXIS_TILT) を使用して取得できます(最初のポインタのショートカットはありません)。

傾斜を使用すると、実際のものにできるだけ近くなるように道具を再現できます。たとえば、傾けた鉛筆で陰影を付けることができます。

画面から約 40 度傾けたタッチペン。
図 4. 垂直位置から約 0.785 ラジアン(45 度)傾けたタッチペン。

ホバー

画面からタッチペンまでの距離は getAxisValue(AXIS_DISTANCE) で取得できます。このメソッドにより返される値の最小値は 0.0(画面に接触)で、タッチペンが画面から離れるほど値が大きくなります。画面とタッチペンのペン先(先端)間のホバー距離は、画面とタッチペンの両方のメーカーによって異なります。実装は異なる場合があるため、アプリの重要機能についてはあまり厳密な値に頼らないようにしてください。

タッチペンのホバーを使用して、ブラシのサイズをプレビューしたり、ボタンが選択されることを明示したりできます。

図 5. 画面にカーソルを合わせたタッチペン。タッチペンが画面の表面に接触していなくても、アプリは反応します。

注: Compose には、UI 要素の状態を変更するための修飾子要素が複数用意されています。

  • hoverable: ポインタの開始イベントと終了イベントを使用してホバーできるようにコンポーネントを設定します。
  • indication: インタラクションが発生したときに、このコンポーネントの視覚効果を描画します。

パーム リジェクション、ナビゲーション、不要な入力

マルチタッチ スクリーンでは、手書き入力中にサポートのためにユーザーが手を置いた場合などに、不要なタップ操作が登録されることがあります。パーム リジェクションは、この動作を検出するメカニズムであり、最後の MotionEvent セットをキャンセルする必要があることを通知します。

そのため、ユーザー入力の履歴を保持することにより、不要なタップを画面から削除し、正当なユーザー入力を再レンダリングできるようにする必要があります。

ACTION_CANCELED と FLAG_CANCELED

ACTION_CANCELFLAG_CANCELED はどちらも、以前の MotionEvent セットを最後の ACTION_DOWN からキャンセルする必要があることを通知するためのものです。たとえば、描画アプリで特定のポインタの最後のストロークを元に戻すことができます。

ACTION_CANCEL

Android 1.0(API レベル 1)で追加

ACTION_CANCEL は、前のモーション イベントのセットをキャンセルする必要があることを示します。

ACTION_CANCEL は、次のいずれかが検出されるとトリガーされます。

  • ナビゲーション ジェスチャー
  • パーム リジェクション

ACTION_CANCEL がトリガーされたら、getPointerId(getActionIndex()) でアクティブなポインタを特定する必要があります。次に、そのポインタで作成したストロークを入力履歴から削除し、シーンを再レンダリングします。

FLAG_CANCELED

Android 13(API レベル 33)で追加

FLAG_CANCELED は、ポインタの上移動が、意図しないタップであったことを示します。このフラグは通常、ユーザーが誤って画面に触れたときに発生します。たとえば、ユーザーがデバイスをつかんだり、手のひらを画面に置いた場合などです。

フラグの値には、次の方法でアクセスできます。

Kotlin

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Java

boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;

このフラグが設定されている場合、このポインタの最後の ACTION_DOWN から、最後の MotionEvent セットを元に戻す必要があります。

ACTION_CANCEL と同様に、ポインタは getPointerId(actionIndex) で見つけることができます。

図 6. タッチペンのストロークと手のひらでのタップ操作により MotionEvent セットが作成されます。手のひらでのタップはキャンセルされ、ディスプレイが再レンダリングされます。

全画面表示、エッジ ツー エッジ、ナビゲーション ジェスチャー

アプリが全画面表示中で、描画アプリやメモ用アプリのキャンバスなどの操作可能な要素が端付近にある場合、画面の下からスワイプして、ナビゲーションを表示したり、アプリをバックグラウンドに移動したりすると、キャンバスが意図せずタップされることがあります。

図 7. スワイプ操作でアプリをバックグラウンドに移動します。

アプリでの操作により不要なタップがトリガーされないようにするには、インセットACTION_CANCEL を使用します。

上記のパーム リジェクション、ナビゲーション、不要な入力もご覧ください。

WindowInsetsControllersetSystemBarsBehavior() メソッドと BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE を使用すると、ナビゲーション ジェスチャーによって不要なタッチイベントが発生しないようにできます。

Kotlin

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Java

// Configure the behavior of the hidden system bars.
windowInsetsController.setSystemBarsBehavior(
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);

インセットとジェスチャー管理について詳しくは、以下をご覧ください。

低レイテンシ

レイテンシとは、ハードウェア、システム、アプリケーションがユーザー入力を処理してレンダリングするのに必要な時間です。

レイテンシ = ハードウェアと OS の入力処理 + アプリ処理 + システム合成 + ハードウェアのレンダリング

レイテンシが発生すると、レンダリングされたストロークはタッチペンの位置より遅れることになります。レンダリングされたストロークとタッチペンの位置の差がレイテンシを表します。
図 8. レイテンシが発生すると、レンダリングされたストロークはタッチペンの位置より遅れることになります。

レイテンシの原因

  • タッチスクリーン(ハードウェア)へのタッチペンの登録: タッチペンと OS の通信により登録および同期する際の最初のワイヤレス接続。
  • タップのサンプリング レート(ハードウェア): タッチスクリーンが 1 秒間にポインタに接触している回数(60~1,000 Hz)。
  • 入力処理(アプリ): ユーザー入力への色、グラフィック エフェクト、変換の適用。
  • グラフィック レンダリング(OS + ハードウェア): バッファ スワップ、ハードウェア処理。

低レイテンシのグラフィックス

Jetpack 低レイテンシ グラフィック ライブラリを使用すると、ユーザー入力から画面上のレンダリングまでの処理時間が短縮されます。

このライブラリは、マルチバッファ レンダリングを回避し、画面に直接書き込むフロント バッファ レンダリング手法を利用することで、処理時間を短縮します。

フロント バッファのレンダリング

フロント バッファは、画面がレンダリングに使用するメモリです。最も近いアプリが画面に直接描画できます。低レイテンシ ライブラリにより、アプリはフロント バッファに直接レンダリングできます。これにより、通常のマルチバッファ レンダリングやダブルバッファ レンダリング(最も一般的な例)でバッファ スワップが防止され、パフォーマンスが向上します。

アプリは画面バッファへの書き込みと、画面バッファからの読み取りを行います。
図 9. フロント バッファのレンダリング。
アプリがマルチバッファに書き込み、マルチバッファが画面バッファとスワップします。アプリが画面バッファから読み取ります。
図 10. マルチバッファ レンダリング。

フロント バッファ レンダリングは画面の小さな領域をレンダリングする優れた手法ですが、画面全体を更新するために使用するものではありません。フロント バッファ レンダリングでは、アプリはディスプレイが読み取るバッファにコンテンツをレンダリングします。その結果、アーティファクトのレンダリングやテアリングが発生する可能性があります(下記をご覧ください)。

低レイテンシ ライブラリは Android 10(API レベル 29)以降と、Android 10(API レベル 29)以降を搭載した ChromeOS デバイスで利用できます。

依存関係

低レイテンシ ライブラリは、フロント バッファ レンダリングを実装するためのコンポーネントを提供します。このライブラリは、アプリのモジュール build.gradle ファイルに依存関係として追加されています。

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

GLFrontBufferRenderer のコールバック

低レイテンシ ライブラリには、次のメソッドを定義する GLFrontBufferRenderer.Callback インターフェースが含まれています。

低レイテンシ ライブラリは、GLFrontBufferRenderer で使用するデータに関係なく使用できます。

しかし、ライブラリはデータを何百ものデータポイントのストリームとして処理します。そのため、メモリ使用量と割り当てを最適化するようにデータを設計してください。

コールバック

レンダリング コールバックを有効にするには、GLFrontBufferedRenderer.Callback を実装し、onDrawFrontBufferedLayer()onDrawDoubleBufferedLayer() をオーバーライドします。GLFrontBufferedRenderer では、コールバックを使用して、可能な限り最適化された方法でデータをレンダリングします。

Kotlin

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {

   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }

   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}

Java

GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks =
    new GLFrontBufferedRenderer.Callback<DATA_TYPE>() {
        @Override
        public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager,
            @NonNull BufferInfo bufferInfo,
            @NonNull float[] transform,
            DATA_TYPE data_type) {
                // OpenGL for front buffer, short, affecting small area of the screen.
        }

    @Override
    public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager,
        @NonNull BufferInfo bufferInfo,
        @NonNull float[] transform,
        @NonNull Collection<? extends DATA_TYPE> collection) {
            // OpenGL full scene rendering.
    }
};
GLFrontBufferedRenderer のインスタンスを宣言する

GLFrontBufferedRenderer を準備するには、先ほど作成した SurfaceView とコールバックを指定します。GLFrontBufferedRenderer は、コールバックを使用してフロント バッファとダブルバッファへのレンダリングを最適化します。

Kotlin

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)

Java

GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer =
    new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
レンダリング

フロント バッファ レンダリングは renderFrontBufferedLayer() メソッドを呼び出すと開始され、これにより onDrawFrontBufferedLayer() コールバックがトリガーされます。

commit() 関数を呼び出すと、ダブルバッファ レンダリングが再開され、onDrawMultiDoubleBufferedLayer() コールバックがトリガーされます。

次の例では、ユーザーが画面上で描画を開始し(ACTION_DOWN)、ポインタを動かすと(ACTION_MOVE)、プロセスがフロント バッファにレンダリングされます(高速レンダリング)。ポインタが画面の表面を離れると、プロセスはダブルバッファにレンダリングされます(ACTION_UP)。

requestUnbufferedDispatch() を使用すると、入力システムがモーション イベントをバッチ処理せずに、利用可能になったらすぐに配信するようリクエストできます。

Kotlin

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

Java

switch (motionEvent.getAction()) {
   case MotionEvent.ACTION_DOWN: {
       // Deliver input events as soon as they arrive.
       surfaceView.requestUnbufferedDispatch(motionEvent);

       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE);
   }
   break;
   case MotionEvent.ACTION_MOVE: {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE);
   }
   break;
   case MotionEvent.ACTION_UP: {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit();
   }
   break;
   case MotionEvent.ACTION_CANCEL: {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel();
   }
   break;
}

レンダリングですべきこと、すべきでないこと

すべきこと

画面の小さな部分、手書き、描画、スケッチ。

すべきでないこと

全画面表示の更新、パン、ズーム。テアリングが発生する場合があります。

テアリング

テアリングは、画面の更新と画面バッファの変更が同時に行われると発生します。画面の一部に新しいデータが表示され、別の部分には古いデータが表示されます。

画面更新時のテアリングによって、Android の画像の上部と下部がずれます。
図 11. 画面の上部から下部に向かって更新が進むとテアリングが発生します。

モーション予測

Jetpack モーション予測ライブラリは、ユーザーのストロークパスを推定し、レンダラに一時的な人工的なポイントを提供することで、認識されるレイテンシを短縮します。

モーション予測ライブラリは、実際のユーザー入力を MotionEvent オブジェクトとして取得します。これらのオブジェクトには、x 座標、y 座標、圧力、時刻に関する情報が含まれ、モーション予測子はこの情報を使用して将来の MotionEvent オブジェクトを予測します。

予測された MotionEvent オブジェクトはあくまで推定です。予測イベントを使用すると認識されるレイテンシを短縮できますが、受信した予測データは実際の MotionEvent データに置き換える必要があります。

モーション予測ライブラリは、Android 4.4(API レベル 19)以降と、Android 9(API レベル 28)以降を搭載した ChromeOS デバイスで利用できます。

レイテンシが発生すると、レンダリングされたストロークはタッチペンの位置より遅れることになります。ストロークとタッチペンの位置の差は予測ポイントで埋めます。残りの差が、認識されるレイテンシです。
図 12. モーション予測によってレイテンシが短縮されました。

依存関係

モーション予測ライブラリにより、予測の実装が提供されます。このライブラリは、アプリのモジュール build.gradle ファイルに依存関係として追加されています。

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

実装

モーション予測ライブラリには、次のメソッドを定義する MotionEventPredictor インターフェースが含まれています。

  • record(): MotionEvent オブジェクトをユーザーのアクションの記録として保存します。
  • predict(): 予測される MotionEvent を返します。
MotionEventPredictor インスタンスを宣言する

Kotlin

var motionEventPredictor = MotionEventPredictor.newInstance(view)

Java

MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
予測子にデータをフィードする

Kotlin

motionEventPredictor.record(motionEvent)

Java

motionEventPredictor.record(motionEvent);
予測

Kotlin

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

Java

switch (motionEvent.getAction()) {
   case MotionEvent.ACTION_MOVE: {
       MotionEvent predictedMotionEvent = motionEventPredictor.predict();
       if(predictedMotionEvent != null) {
           // use predicted MotionEvent to inject a new artificial point
       }
   }
   break;
}

モーション予測ですべきこととすべきでないこと

すべきこと

新しい予測ポイントが追加された時点で、予測ポイントを削除する。

すべきでないこと

最終的なレンダリングに予測ポイントを使用しない。

メモ作成アプリ

ChromeOS では、アプリでのメモ作成アクションを宣言できます。

ChromeOS でアプリをメモ作成アプリとして登録するには、入力の互換性をご覧ください。

Android でアプリをメモ作成機能として登録するには、メモ作成アプリを作成するをご覧ください。

Android 14(API レベル 34)では、ロック画面でメモ作成アクティビティを開始できる ACTION_CREATE_NOTE インテントが導入されました。

ML Kit によるデジタルインク認識

ML Kit のデジタルインク認識機能を使用すると、アプリは数百もの言語で、デジタル表示面の手書き文字を認識できます。スケッチを分類することもできます。

ML Kit では、Ink.Stroke.Builder クラスにより Ink オブジェクトを作成できます。このオブジェクトを機械学習モデルが処理して、手書き文字をテキストに変換します。

このモデルでは、手書き入力の認識に加え、削除や円などの操作を認識できます。

詳しくは、デジタルインク認識をご覧ください。

参考情報

デベロッパー ガイド

Codelabs