跟踪触摸和指针移动

尝试 Compose 的方式
Jetpack Compose 是推荐用于 Android 的界面工具包。了解如何在 Compose 中使用触摸和输入。

本课程介绍如何在触摸事件中跟踪移动。

每当当前触摸接触点的位置、压力或尺寸发生变化时,都会触发新的 onTouchEvent(),伴随 ACTION_MOVE 事件。如检测常见手势中所述,所有这些事件都记录在 onTouchEvent()MotionEvent 参数中。

由于基于手指的触摸并不总是最精确的交互形式,因此检测触摸事件通常更多基于移动而非简单接触。为了帮助应用区分基于移动的手势(例如滑动)和非移动手势(例如单次点按),Android 引入了*触摸容差(touch slop)*的概念。触摸容差是指用户触摸可以在手势被解释为基于移动的手势之前漂移的像素距离。有关此主题的更多信息,请参阅在 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)及更高版本中提供的一项功能,通过将所有鼠标事件传递到应用中具有焦点的 View 来提供此控制。

请求指针捕获

应用中的 View 只能在其包含 View 的层次结构具有焦点时请求指针捕获。因此,当 View 上发生特定用户操作时(例如在 onClick() 事件期间或在 Activity 的 onWindowFocusChanged() 事件处理程序中),请求指针捕获。

要请求指针捕获,请在 View 上调用 requestPointerCapture() 方法。以下代码示例展示了用户点击 View 时如何请求指针捕获:

Kotlin

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

Java

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

请求捕获指针成功后,Android 会调用 onPointerCaptureChange(true)。系统将鼠标事件传递到您应用中具有焦点的 View,只要它与请求捕获的 View 位于同一 View 层次结构中。其他应用将停止接收鼠标事件,直到释放捕获为止,包括 ACTION_OUTSIDE 事件。Android 会正常传递非鼠标来源的指针事件,但鼠标指针将不再可见。

处理捕获的指针事件

一旦 View 成功获取指针捕获,Android 就会传递鼠标事件。您的焦点 View 可以通过执行以下任一任务来处理事件:

以下代码示例展示了如何实现 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;
  }
});

无论您使用自定义 View 还是注册监听器,您的 View 都会收到一个 MotionEvent,其中包含指定相对移动(例如 X 或 Y 增量)的指针坐标,类似于轨迹球设备提供的坐标。您可以使用 getX()getY() 检索坐标。

释放指针捕获

应用中的 View 可以通过调用 releasePointerCapture() 来释放指针捕获,如以下代码示例所示:

Kotlin

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

Java

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

系统可以在您未显式调用 releasePointerCapture() 的情况下从 View 撤销捕获,通常是因为包含请求捕获的 View 的 View 层次结构失去了焦点。