跟踪轻触和指针移动

这节课介绍了如何跟踪轻触事件中的移动操作。

只要当前轻触的接触位置、压力或大小发生变化,系统就会使用 ACTION_MOVE 事件触发新的 onTouchEvent()。如检测常用手势中所述,所有这些事件都记录在 onTouchEvent()MotionEvent 参数中。

由于基于手指的轻触并不总是最精确的互动形式,因此检测轻触事件时通常更注重移动操作,而不仅仅是接触。为了帮助应用区分移动类手势(例如滑动)和非移动手势(例如点按一次),Android 引入了“touch slop”的概念。Touch slop 是指在系统将手势解读为基于移动的手势之前,用户的轻触手势可以滑动的距离(以像素为单位)。有关此内容的详细讨论,请参阅在 ViewGroup 中管理轻触事件

根据应用的需求,您可以通过多种不同的方式跟踪手势中的移动操作。例如:

  • 指针的起始位置和结束位置(例如,将屏幕中的对象从 A 点移到 B 点)。
  • 指针行进的方向(由 x 和 y 坐标确定)。
  • 历史事件。您可以通过调用 MotionEvent 方法 getHistorySize() 确定手势历史事件的大小。然后,您可以通过 MotionEvent 的 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
                        // 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
                    // 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() 事件过程中,或在 Activity 的 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 便会开始传递鼠标事件。聚焦的视图可以通过执行以下其中一项任务来处理事件:

  1. 如果您使用的是自定义视图,请替换 onCapturedPointerEvent(MotionEvent)
  2. 否则,请注册 OnCapturedPointerListener

以下代码示例展示了如何实现 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 was
        // 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 was
      // 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 was
        // 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 was
        // successfully processed
        return true;
      }
    });
    

无论您是使用自定义视图还是注册监听器,您的视图都会收到包含指针坐标的 MotionEvent,该坐标会指定相对移动(例如 X/Y 增量),类似于轨迹球设备传递的坐标。您可以使用 getX()getY() 检索坐标。

释放捕获的指针

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

Kotlin

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

Java

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

系统可以在您未显式调用 releasePointerCapture() 的情况下将捕获的指针从视图中移走,这通常是因为请求捕获的视图所在的视图层次结构已失去焦点。