高度なタッチペン機能

Android と ChromeOS では、さまざまな API を使用して、以下の機能を提供するアプリを構築できます。 優れたタッチペン エクスペリエンスを提供します。「 MotionEvent クラスが公開 画面でのタッチペン操作に関する情報(タッチペンの圧力など) 画面の向き、傾斜、ホバー、手のひら検出があります。低レイテンシのグラフィックスとモーション 予測ライブラリでは、画面上のタッチペンのレンダリングが ペンと紙のように自然に操作できます。

MotionEvent

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

イベントデータ

MotionEvent データにアクセスするには、コンポーネントに pointerInput 修飾子を追加します。

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

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

  • アクション: デバイスの物理的な操作(画面のタップ、 画面上でのポインタの移動、画面上でのホバー サーフェス
  • ポインタ: 画面を操作するオブジェクトの識別子(指、 タッチペン、マウス
  • 軸: データの種類 - 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 本目の指が画面に接触する位置。

ポインタ インデックスの範囲は、0 から、 MotionEvent#pointerCount() 減らします。

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

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

ポインタについて詳しくは、マルチタッチを処理するをご覧ください。 操作します

タッチペン入力

タッチペン入力をフィルタするには、 TOOL_TYPE_STYLUS:

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

タッチペンが消しゴムとして使用されていることも TOOL_TYPE_ERASER:

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

タッチペンの軸データ

ACTION_DOWNACTION_MOVE は、タッチペンに関する軸データ、つまり x と y 座標、圧力、向き、傾斜、ホバーです。

このデータへのアクセスを可能にするために、MotionEvent API には getAxisValue(int), ここで、パラメータは次のいずれかの軸識別子です。

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

たとえば、MotionEvent.getAxisValue(AXIS_X) は、 最初のポインタです。

マルチタッチの処理もご覧ください。 操作します

職位

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

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

気圧

次のように、ポインタの圧力を取得できます。 MotionEvent#getAxisValue(AXIS_PRESSURE)。最初のポインタの場合は、 MotionEvent#getPressure()

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

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

向き

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

ポインタの向きは、getAxisValue(AXIS_ORIENTATION) または getOrientation() (最初のポインタの場合)。

タッチペンの場合、向きは 0 ~円周率のラジアン値として返されます。 0 から -pi 反時計回りに変更できます。

向きを使用すると、実際のブラシを実装できます。たとえば、 タッチペンはフラットブラシを表し、フラットブラシの幅は タッチペンの向きを変更できます。

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

傾斜

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

傾斜は、タッチペンの正の角度をラジアン単位で返します。ゼロは (つまり画面に対して垂直方向であり、円周方向は平坦な pi/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 は、ポインタの上移動がユーザーによる意図しないタップであることを示します。フラグは、 通常は、ユーザーが誤って画面に触れたときに設定されます。たとえば、画面をグリップした場合などです。 画面に手のひらを当てます。

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

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

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

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

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

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

アプリが全画面表示で、操作可能な要素が端付近にある場合は、 描画アプリまたはメモ作成アプリのキャンバスで画面の下部からスワイプすると、 ナビゲーションを表示したり、アプリをバックグラウンドに移動したりすると、 キャンバスに不要な手を加えられます

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

アプリでの操作によって不要なタップがトリガーされないようにするには、以下を行います。 インセットACTION_CANCEL

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

こちらの setSystemBarsBehavior() メソッドと BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE / WindowInsetsController ナビゲーション ジェスチャーによって不要なタッチイベントが発生しないようにすることができます。

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

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

低遅延

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

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

  • ハードウェア レンダリング
で確認できます。 <ph type="x-smartling-placeholder">
</ph> レイテンシが発生すると、レンダリングされたストロークはタッチペンの位置より遅れることになります。レンダリングされたストロークとタッチペンの位置の差がレイテンシを表します。 <ph type="x-smartling-placeholder">
</ph> 図 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 はコールバックを使用して、 可能になっています。

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.
   }
}
GLFrontBufferedRenderer のインスタンスを宣言する

GLFrontBufferedRenderer を準備するには、SurfaceView と 前に作成したコールバックを使用します。GLFrontBufferedRenderer はレンダリングを最適化します。 をフロント バッファとダブル バッファにアタッチします。

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

フロントバッファ レンダリングは、 renderFrontBufferedLayer() メソッドを呼び出し、onDrawFrontBufferedLayer() コールバックをトリガーします。

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

以下の例では、プロセスがフロント バッファにレンダリングされます(高速 ユーザーが画面への描画(ACTION_DOWN)を開始し、 カーソルを合わせます(ACTION_MOVE)。プロセスがダブルバッファにレンダリングされる ポインタが画面の表面から出たとき(ACTION_UP

次を使用: requestUnbufferedDispatch() 入力システムがモーション イベントをバッチ処理するのではなく、 すぐに次のことが可能になります。

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()
   }
}

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

✓ 推奨

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

🔒? すべきでないこと

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

テアリング

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

画面更新時のテアリングによって、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 インスタンスを宣言する
var motionEventPredictor = MotionEventPredictor.newInstance(view)
予測子にデータをフィードする
motionEventPredictor.record(motionEvent)
予測

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

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

✓ 推奨

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

🔒? すべきでないこと

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

メモ作成アプリ

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

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

Android でアプリをメモ作成として登録するには、メモ作成を作成する アプリ

Android 14(API レベル 34)では、 ACTION_CREATE_NOTE インテント。アプリがロックに対してメモ作成アクティビティを開始できるようにします。 表示されます。

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

ML Kit デジタルインク 認識、 アプリは、数百もの数千ものデジタル サーフェス上の手書き文字を認識できます。 対応しています。スケッチを分類することもできます。

ML Kit には Ink.Stroke.Builder クラスで ML モデルで処理できる Ink オブジェクトを作成します。 手書き文字をテキストに変換できます。

モデルは手書き入力の認識に加え 操作 使用できます。

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

参考情報

デベロッパー ガイド

Codelabs