多点触控手指跟踪

本主题演示如何跟踪来自多个手指的触摸事件

有时,多点触控应用程序需要跟踪各个手指在屏幕上同时移动的情况。 一种典型的应用是手指绘画程序。 你希望用户不仅能够使用单根手指进行绘画,而且还能同时使用多根手指进行绘画。 当程序处理多个触摸事件时,它需要区分哪些事件对应于每个手指。 Android 提供用于此目的的 ID 代码,但获取和处理该代码可能有点棘手。

对于与特定手指关联的所有事件,ID 代码保持不变。 当手指首次触摸屏幕时,会分配 ID 代码,并在手指从屏幕中抬起后变为无效。 这些 ID 代码通常是非常小的整数,Android 会将其重新用于以后的触摸事件。

跟踪各个手指的程序几乎始终维护一个用于触摸跟踪的字典。 字典键是标识特定手指的 ID 代码。 字典值取决于应用程序。 在 FingerPaint 程序中,每个手指笔划(从触摸到释放)都与一个对象相关联,该对象包含渲染该手指绘制的线条所需的所有信息。 该程序为此定义了一个小型 FingerPaintPolyline 类:

class FingerPaintPolyline
{
    public FingerPaintPolyline()
    {
        Path = new Path();
    }

    public Color Color { set; get; }

    public float StrokeWidth { set; get; }

    public Path Path { private set; get; }
}

每个折线都有一种颜色、笔划宽度和一个 Android 图形 Path 对象,用于在绘制线条时累积和呈现线条的多个点。

下面所示代码的其余部分包含在名为 FingerPaintCanvasViewView 派生体中。 当一根或多根手指主动绘制对象时,该类维护一个 FingerPaintPolyline 类型的对象字典:

Dictionary<int, FingerPaintPolyline> inProgressPolylines = new Dictionary<int, FingerPaintPolyline>();

此字典允许视图快速获取与特定手指关联的 FingerPaintPolyline 信息。

FingerPaintCanvasView 类还为已完成的折线维护一个 List 对象:

List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();

List 中的对象顺序与它们的绘制顺序相同。

FingerPaintCanvasView 重写由 View 定义的两种方法:OnDrawOnTouchEvent。 在其 OnDraw 重写中,视图绘制已完成的折线,然后绘制正在进行的折线。

OnTouchEvent 方法的重写首先从 ActionIndex 属性获取 pointerIndex 值。 此 ActionIndex 值区分多个手指,但它在多个事件中不一致。 因此,请使用 pointerIndexGetPointerId 方法获取指针 id 值。 此 ID 在多个事件中一致

public override bool OnTouchEvent(MotionEvent args)
{
    // Get the pointer index
    int pointerIndex = args.ActionIndex;

    // Get the id to identify a finger over the course of its progress
    int id = args.GetPointerId(pointerIndex);

    // Use ActionMasked here rather than Action to reduce the number of possibilities
    switch (args.ActionMasked)
    {
        // ...
    }

    // Invalidate to update the view
    Invalidate();

    // Request continued touch input
    return true;
}

请注意,重写使用 switch 语句中的 ActionMasked 属性,而不是 Action 属性。 原因如下:

处理多点触控时,Action 属性的值为 MotionEventsAction.Down,第一根手指触摸屏幕,然后 Pointer2Down 的值和 Pointer3Down 值,因为第二根和第三根手指也会触摸屏幕。 当第四根和第五根手指接触时,Action 属性具有数值,这些数值甚至与 MotionEventsAction 枚举的成员不对应! 你需要检查值中的位标志的值来解释它们的含义。

同样,当手指与屏幕保持接触时,Action 属性具有第二和第三根手指的 Pointer2Up 值和 Pointer3Up 值,以及第一根手指的 Up

ActionMasked 属性采用较少的值,因为它旨在与 ActionIndex 属性结合使用,以区分多个手指。 当手指触摸屏幕时,该属性只能等于第一根手指的 MotionEventActions.Down,以及后续手指的 PointerDown。 当手指离开屏幕时,ActionMasked 具有后续手指的 Pointer1Up 值,以及第一根手指的 Up 值。

使用 ActionMasked 时,ActionIndex 区分后续手指触摸和离开屏幕,但通常不需要使用该值,只是作为 MotionEvent 对象中其他方法的参数。 对于多点触控,上述代码中调用了其中最重要的方法之一 GetPointerId。 该方法返回一个值,该值可用于字典键将特定事件与手指相关联。

FingerPaint 程序中的OnTouchEvent 重写通过创建新的 FingerPaintPolyline 对象,并将其添加到字典来处理 MotionEventActions.DownPointerDown 事件:

public override bool OnTouchEvent(MotionEvent args)
{
    // Get the pointer index
    int pointerIndex = args.ActionIndex;

    // Get the id to identify a finger over the course of its progress
    int id = args.GetPointerId(pointerIndex);

    // Use ActionMasked here rather than Action to reduce the number of possibilities
    switch (args.ActionMasked)
    {
        case MotionEventActions.Down:
        case MotionEventActions.PointerDown:

            // Create a Polyline, set the initial point, and store it
            FingerPaintPolyline polyline = new FingerPaintPolyline
            {
                Color = StrokeColor,
                StrokeWidth = StrokeWidth
            };

            polyline.Path.MoveTo(args.GetX(pointerIndex),
                                 args.GetY(pointerIndex));

            inProgressPolylines.Add(id, polyline);
            break;
        // ...
    }
    // ...        
}

请注意,pointerIndex 还用于获取视图中手指的位置。 所有触摸信息都与 pointerIndex 值相关联。 id 通过多个消息唯一标识手指,以便用于创建字典条目。

同样,OnTouchEvent 重写还通过将已完成的折线传输到 completedPolylines 集合来处理 MotionEventActions.UpPointer1Up,以便在 OnDraw 重写期间绘制它们。 该代码还会从字典中删除 id 条目:

public override bool OnTouchEvent(MotionEvent args)
{
    // ...
    switch (args.ActionMasked)
    {
        // ...
        case MotionEventActions.Up:
        case MotionEventActions.Pointer1Up:

            inProgressPolylines[id].Path.LineTo(args.GetX(pointerIndex),
                                                args.GetY(pointerIndex));

            // Transfer the in-progress polyline to a completed polyline
            completedPolylines.Add(inProgressPolylines[id]);
            inProgressPolylines.Remove(id);
            break;

        case MotionEventActions.Cancel:
            inProgressPolylines.Remove(id);
            break;
    }
    // ...        
}

现在来看棘手的部分。

在上下事件之间,通常有许多 MotionEventActions.Move 事件。 这些绑定在对 OnTouchEvent的单个调用中,它们必须以不同于 DownUp 事件的方式进行处理。 必须忽略之前从 ActionIndex 属性获取的 pointerIndex 值。 相反,该方法必须通过循环 0 和 PointerCount 属性来获取多个 pointerIndex 值,然后为每个 pointerIndex 值获取 id

public override bool OnTouchEvent(MotionEvent args)
{
    // ...
    switch (args.ActionMasked)
    {
        // ...
        case MotionEventActions.Move:

            // Multiple Move events are bundled, so handle them differently
            for (pointerIndex = 0; pointerIndex < args.PointerCount; pointerIndex++)
            {
                id = args.GetPointerId(pointerIndex);

                inProgressPolylines[id].Path.LineTo(args.GetX(pointerIndex),
                                                    args.GetY(pointerIndex));
            }
            break;
        // ...
    }
    // ...        
}

这种类型的处理允许 FingerPaint 程序跟踪各个手指并在屏幕上绘制结果:

Example screenshot from FingerPaint example

现在,你已了解如何跟踪屏幕上的各个手指并区分它们。