タップとポインタの動きをトラッキングする

Compose を試す
Jetpack Compose は Android の推奨 UI ツールキットです。Compose でタップと入力を使用する方法について説明します。

このレッスンでは、タッチイベントの動きをトラッキングする方法について説明します。

現在のタッチ接触位置、圧力、サイズが変更されるたびに、ACTION_MOVE イベントによって新しい onTouchEvent() がトリガーされます。一般的な操作を検出するで説明したように、これらのイベントはすべて onTouchEvent()MotionEvent パラメータに記録されます。

指ベースのタップは、必ずしも最も正確なインタラクション形式であるとは限らないため、タッチイベントの検出は、多くの場合、単純な接触よりも動きに基づいて行われます。アプリが移動ベースの操作(スワイプなど)と移動以外のジェスチャー(シングルタップなど)を区別できるように、Android にはタッチスロップの概念があります。タッチスロップとは、ユーザーのタップが動きに基づく操作として解釈される前にユーザーのタップが移動できる距離をピクセル単位で表したものです。このトピックの詳細については、ViewGroup のタッチイベントを管理するをご覧ください。

ジェスチャーでの動きを追跡するには、アプリのニーズに応じていくつかの方法があります。以下に例を示します。

  • ポインタの開始位置と終了位置(画面上のオブジェクトをポイント A からポイント B に移動する場合など)。
  • ポインタの移動方向。X 座標と Y 座標によって決まります。
  • 履歴。操作の履歴サイズを確認するには、MotionEvent メソッドの getHistorySize() を呼び出します。モーション イベントの getHistorical<Value> メソッドを使用して、各イベントの位置、サイズ、時間、圧力を取得できます。履歴は、タップ描画など、ユーザーの指の軌跡をレンダリングする場合に役立ちます。詳しくは、MotionEvent のリファレンスをご覧ください。
  • タッチスクリーン上でのポインタの移動速度。

以下の関連リソースもご覧ください。

トラッキング速度

ポインタの移動距離や方向に基づいた、動きベースの操作を作成できます。しかし、多くの場合、速度はジェスチャーの特性のトラッキングや、ジェスチャーが発生したかどうかを判断する際の決定要因になります。速度の計算を容易にするために、Android には VelocityTracker クラスが用意されています。VelocityTracker は、タッチイベントの速度を追跡するのに役立ちます。これは、フリングなど、速度が操作の基準の一部であるジェスチャーに役立ちます。

以下に、VelocityTracker API のメソッドの目的を示す例を示します。

Kotlin

private const val DEBUG_TAG = "Velocity"

class MainActivity : Activity() {
    private var mVelocityTracker: VelocityTracker? = null

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                // Reset the velocity tracker back to its initial state.
                mVelocityTracker?.clear()
                // If necessary, retrieve a new VelocityTracker object to watch
                // the velocity of a motion.
                mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain()
                // Add a user's movement to the tracker.
                mVelocityTracker?.addMovement(event)
            }
            MotionEvent.ACTION_MOVE -> {
                mVelocityTracker?.apply {
                    val pointerId: Int = event.getPointerId(event.actionIndex)
                    addMovement(event)
                    // When you want to determine the velocity, call
                    // computeCurrentVelocity(). Then, call getXVelocity() and
                    // getYVelocity() to retrieve the velocity for each pointer
                    // ID.
                    computeCurrentVelocity(1000)
                    // Log velocity of pixels per second. It's best practice to
                    // use VelocityTrackerCompat where possible.
                    Log.d("", "X velocity: ${getXVelocity(pointerId)}")
                    Log.d("", "Y velocity: ${getYVelocity(pointerId)}")
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // Return a VelocityTracker object back to be re-used by others.
                mVelocityTracker?.recycle()
                mVelocityTracker = null
            }
        }
        return true
    }
}

Java

public class MainActivity extends Activity {
    private static final String DEBUG_TAG = "Velocity";
        ...
    private VelocityTracker mVelocityTracker = null;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int index = event.getActionIndex();
        int action = event.getActionMasked();
        int pointerId = event.getPointerId(index);

        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if(mVelocityTracker == null) {
                    // Retrieve a new VelocityTracker object to watch the
                    // velocity of a motion.
                    mVelocityTracker = VelocityTracker.obtain();
                }
                else {
                    // Reset the velocity tracker back to its initial state.
                    mVelocityTracker.clear();
                }
                // Add a user's movement to the tracker.
                mVelocityTracker.addMovement(event);
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);
                // When you want to determine the velocity, call
                // computeCurrentVelocity(). Then call getXVelocity() and
                // getYVelocity() to retrieve the velocity for each pointer ID.
                mVelocityTracker.computeCurrentVelocity(1000);
                // Log velocity of pixels per second. It's best practice to use
                // VelocityTrackerCompat where possible.
                Log.d("", "X velocity: " + mVelocityTracker.getXVelocity(pointerId));
                Log.d("", "Y velocity: " + mVelocityTracker.getYVelocity(pointerId));
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // Return a VelocityTracker object back to be re-used by others.
                mVelocityTracker.recycle();
                break;
        }
        return true;
    }
}

ポインタ キャプチャの使用

ゲーム、リモート デスクトップ、仮想化クライアントなど、一部のアプリではマウスポインタを制御して管理できます。ポインタ キャプチャは Android 8.0(API レベル 26)以降で使用できる機能で、すべてのマウスイベントをアプリ内のフォーカスされているビューに配信することで、このコントロールを提供します。

ポインタ キャプチャのリクエスト

アプリ内のビューは、そのビューを含むビュー階層にフォーカスがある場合にのみ、ポインタ キャプチャをリクエストできます。このため、onClick() イベント中やアクティビティの onWindowFocusChanged() イベント ハンドラ中など、ビューで特定のユーザー アクションが発生したときに、ポインタのキャプチャをリクエストします。

ポインタのキャプチャをリクエストするには、ビューで requestPointerCapture() メソッドを呼び出します。次のコード例は、ユーザーがビューをクリックしたときにポインタ キャプチャをリクエストする方法を示しています。

Kotlin

fun onClick(view: View) {
    view.requestPointerCapture()
}

Java

@Override
public void onClick(View view) {
    view.requestPointerCapture();
}

ポインタのキャプチャ リクエストが成功すると、Android は onPointerCaptureChange(true) を呼び出します。キャプチャをリクエストしたビューと同じビュー階層内にある限り、システムはアプリのフォーカスされているビューにマウスイベントを配信します。他のアプリは、キャプチャがリリースされるまでマウスイベント(ACTION_OUTSIDE イベントなど)の受信を停止します。Android はマウス以外のソースからのポインタ イベントを通常どおり配信しますが、マウスポインタは表示されなくなっています。

キャプチャされたポインタ イベントの処理

ビューがポインタ キャプチャを正常に取得すると、Android はマウスイベントを配信します。フォーカスされているビューで、次のいずれかのタスクを実行することで、イベントを処理できます。

次のコード例は、onCapturedPointerEvent(MotionEvent) の実装方法を示しています。

Kotlin

override fun onCapturedPointerEvent(motionEvent: MotionEvent): Boolean {
    // Get the coordinates required by your app.
    val verticalOffset: Float = motionEvent.y
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    return true
}

Java

@Override
public boolean onCapturedPointerEvent(MotionEvent motionEvent) {
  // Get the coordinates required by your app.
  float verticalOffset = motionEvent.getY();
  // Use the coordinates to update your view and return true if the event is
  // successfully processed.
  return true;
}

次のコードサンプルは、OnCapturedPointerListener を登録する方法を示しています。

Kotlin

myView.setOnCapturedPointerListener { view, motionEvent ->
    // Get the coordinates required by your app.
    val horizontalOffset: Float = motionEvent.x
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    true
}

Java

myView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
  @Override
  public boolean onCapturedPointer (View view, MotionEvent motionEvent) {
    // Get the coordinates required by your app.
    float horizontalOffset = motionEvent.getX();
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    return true;
  }
});

カスタムビューを使用する場合も、リスナーを登録する場合も、ビューは、トラックボール デバイスによって配信される座標と同様に、X デルタや Y デルタなどの相対的な動きを指定するポインタ座標を含む MotionEvent を受け取ります。座標を取得するには、getX()getY() を使用します。

ポインタ キャプチャの解除

アプリ内のビューは、次のコードサンプルに示すように、releasePointerCapture() を呼び出すことでポインタのキャプチャを解放できます。

Kotlin

override fun onClick(view: View) {
    view.releasePointerCapture()
}

Java

@Override
public void onClick(View view) {
    view.releasePointerCapture();
}

システムは、releasePointerCapture() を明示的に呼び出さなくてもビューからキャプチャを除去できます。これは一般的に、キャプチャをリクエストしたビューを含むビュー階層がフォーカスを失うためです。