处理多点触控手势

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

多点触控手势是指多个指针(手指)同时点击屏幕的情况。本文档介绍了如何检测涉及多个指针的手势。

追踪多个指针

当多个指针同时点击屏幕时,系统会生成以下触摸事件:

  • ACTION_DOWN:在第一个指针点击屏幕时发送。这标志着手势的开始。该指针的数据始终位于 MotionEvent 的索引 0 处。
  • ACTION_POINTER_DOWN:在第一个指针之后,有额外的指针进入屏幕时发送。您可以使用 getActionIndex() 获取刚刚按下的指针索引。
  • ACTION_MOVE:在手势发生变化时发送,涉及任意数量的指针。
  • ACTION_POINTER_UP:在非主指针抬起时发送。您可以使用 getActionIndex() 获取刚刚抬起的指针索引。
  • ACTION_UP:在最后一个指针离开屏幕时发送。
  • ACTION_CANCEL:表示整个手势(包括所有指针)已被取消。

手势的开始与结束

手势是一系列事件,以 ACTION_DOWN 事件开始,并以 ACTION_UPACTION_CANCEL 事件结束。同一时间只能有一个活跃的手势。DOWN、MOVE、UP 和 CANCEL 动作适用于整个手势。例如,带有 ACTION_MOVE 的事件可以指示当前按下的所有指针的移动。

保持对指针的追踪

MotionEvent 中,请使用指针的索引(index)和 ID 来追踪各个指针的位置。

  • 索引 (Index)MotionEvent 将指针信息存储在一个数组中。指针的索引即其在该数组中的位置。大多数 MotionEvent 方法都接收指针索引作为参数,而不是指针 ID。
  • ID:每个指针还有一个映射 ID,该 ID 在触摸事件中保持不变,以便在整个手势过程中追踪单个指针。

单个指针在运动事件中出现的顺序是不确定的。因此,指针的索引可能会从一个事件变到下一个事件,但只要指针保持活跃,其指针 ID 保证保持不变。请使用 getPointerId() 方法获取指针的 ID,以便在手势的所有后续运动事件中追踪该指针。然后,对于后续的运动事件,请使用 findPointerIndex() 方法在该运动事件中获取特定指针 ID 对应的指针索引。例如:

Kotlin

private var mActivePointerId: Int = 0

override fun onTouchEvent(event: MotionEvent): Boolean {
    ...
    // Get the pointer ID.
    mActivePointerId = event.getPointerId(0)

    // ... Many touch events later...

    // Use the pointer ID to find the index of the active pointer
    // and fetch its position.
    val (x: Float, y: Float) = event.findPointerIndex(mActivePointerId).let { pointerIndex ->
        // Get the pointer's current position.
        event.getX(pointerIndex) to event.getY(pointerIndex)
    }
    ...
}

Java

private int mActivePointerId;

public boolean onTouchEvent(MotionEvent event) {
    ...
    // Get the pointer ID.
    mActivePointerId = event.getPointerId(0);

    // ... Many touch events later...

    // Use the pointer ID to find the index of the active pointer
    // and fetch its position.
    int pointerIndex = event.findPointerIndex(mActivePointerId);
    // Get the pointer's current position.
    float x = event.getX(pointerIndex);
    float y = event.getY(pointerIndex);
    ...
}

为了支持多个触摸指针,您可以在各个 ACTION_POINTER_DOWNACTION_DOWN 事件发生时,缓存所有带有 ID 的活跃指针。在 ACTION_POINTER_UPACTION_UP 事件发生时,从缓存中移除这些指针。您可能会发现这些缓存的 ID 对于正确处理其他动作事件很有帮助。例如,在处理 ACTION_MOVE 事件时,找到每个缓存的活跃指针 ID 对应的索引,使用 getX()getY() 函数检索指针的坐标,然后将这些坐标与缓存的坐标进行比较,以发现哪些指针发生了移动。

仅在 ACTION_POINTER_UPACTION_POINTER_DOWN 事件中使用 getActionIndex() 函数。请勿在 ACTION_MOVE 事件中使用此函数,因为该函数在此情况下总是返回 0

检索 MotionEvent 动作

使用 getActionMasked() 方法或兼容版本 MotionEventCompat.getActionMasked() 来检索 MotionEvent 的动作。与早期的 getAction() 方法不同,getActionMasked() 旨在处理多个指针。它返回不包含指针索引的动作。对于具有有效指针索引的动作,请使用 getActionIndex() 返回与该动作关联的指针索引,如下面的代码片段所示:

Kotlin

val (xPos: Int, yPos: Int) = MotionEventCompat.getActionMasked(event).let { action ->
    Log.d(DEBUG_TAG, "The action is ${actionToString(action)}")
    // Get the index of the pointer associated with the action.
    MotionEventCompat.getActionIndex(event).let { index ->
        // The coordinates of the current screen contact, relative to
        // the responding View or Activity.
        MotionEventCompat.getX(event, index).toInt() to MotionEventCompat.getY(event, index).toInt()
    }
}

if (event.pointerCount > 1) {
    Log.d(DEBUG_TAG, "Multitouch event")

} else {
    // Single touch event.
    Log.d(DEBUG_TAG, "Single touch event")
}

...

// Given an action int, returns a string description.
fun actionToString(action: Int): String {
    return when (action) {
        MotionEvent.ACTION_DOWN -> "Down"
        MotionEvent.ACTION_MOVE -> "Move"
        MotionEvent.ACTION_POINTER_DOWN -> "Pointer Down"
        MotionEvent.ACTION_UP -> "Up"
        MotionEvent.ACTION_POINTER_UP -> "Pointer Up"
        MotionEvent.ACTION_OUTSIDE -> "Outside"
        MotionEvent.ACTION_CANCEL -> "Cancel"
        else -> ""
    }
}

Java

int action = MotionEventCompat.getActionMasked(event);
// Get the index of the pointer associated with the action.
int index = MotionEventCompat.getActionIndex(event);
int xPos = -1;
int yPos = -1;

Log.d(DEBUG_TAG,"The action is " + actionToString(action));

if (event.getPointerCount() > 1) {
    Log.d(DEBUG_TAG,"Multitouch event");
    // The coordinates of the current screen contact, relative to
    // the responding View or Activity.
    xPos = (int)MotionEventCompat.getX(event, index);
    yPos = (int)MotionEventCompat.getY(event, index);

} else {
    // Single touch event.
    Log.d(DEBUG_TAG,"Single touch event");
    xPos = (int)MotionEventCompat.getX(event, index);
    yPos = (int)MotionEventCompat.getY(event, index);
}
...

// Given an action int, returns a string description
public static String actionToString(int action) {
    switch (action) {

        case MotionEvent.ACTION_DOWN: return "Down";
	case MotionEvent.ACTION_MOVE: return "Move";
	case MotionEvent.ACTION_POINTER_DOWN: return "Pointer Down";
	case MotionEvent.ACTION_UP: return "Up";
	case MotionEvent.ACTION_POINTER_UP: return "Pointer Up";
	case MotionEvent.ACTION_OUTSIDE: return "Outside";
	case MotionEvent.ACTION_CANCEL: return "Cancel";
    }
    return "";
}
图 1. 多点触控绘制模式。

其他资源

有关输入事件的更多信息,请参阅以下参考资料: