Share via


Seguimiento multitáctil de los dedos

En este tema se muestra cómo realizar un seguimiento de eventos táctiles de varios dedos

Hay ocasiones en las que una aplicación multitáctil necesita realizar un seguimiento de los dedos de forma individual a medida que se mueven simultáneamente en la pantalla. Una aplicación típica es un programa de pintura de dedos. Querrá que el usuario pueda dibujar con un solo dedo, pero también con varios dedos a la vez. A medida que el programa procesa varios eventos táctiles, debe distinguir qué eventos corresponden a cada dedo. Android proporciona un código de identificador para este propósito, pero obtener y controlar ese código puede ser un poco complicado.

Para todos los eventos asociados a un dedo determinado, el código de identificador sigue siendo el mismo. El código de identificador se asigna cuando un dedo toca por primera vez la pantalla y deja de ser válido después de que el dedo levante desde la pantalla. Estos códigos de identificador suelen ser enteros muy pequeños y Android los reutiliza para eventos táctiles posteriores.

Casi siempre, un programa que realiza un seguimiento de los dedos de forma individual mantiene un diccionario para el seguimiento táctil. La clave de diccionario es el código de identificador que identifica un dedo determinado. El valor del diccionario dependerá de la aplicación. En el ejemplo de FingerPaint, cada trazo de dedo (desde la entrada táctil a la liberación) está asociado a un objeto que contiene toda la información necesaria para representar la línea dibujada con ese dedo. El programa define una clase pequeña FingerPaintPolyline para este propósito:

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

    public Color Color { set; get; }

    public float StrokeWidth { set; get; }

    public Path Path { private set; get; }
}

Cada polilínea tiene un color, un ancho de trazo y un objeto gráfico Path de Android para acumular y representar varios puntos de la línea a medida que se dibuja.

El resto del código que se muestra a continuación se incluye en un elemento View derivado denominado FingerPaintCanvasView. Esa clase mantiene un diccionario de objetos de tipo FingerPaintPolyline durante el tiempo en que uno o varios dedos dibujan activamente:

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

Este diccionario permite que la vista obtenga rápidamente la información de FingerPaintPolyline asociada a un dedo determinado.

La clase FingerPaintCanvasView también mantiene un objeto List para las polilíneas que se hayan completado:

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

Los objetos de este objeto List están en el mismo orden en que se dibujaron.

FingerPaintCanvasView invalida dos métodos definidos por View: OnDraw y OnTouchEvent. En la invalidación de OnDraw, la vista dibuja las polilíneas completadas y, a continuación, dibuja las polilíneas en curso.

La invalidación del método OnTouchEvent comienza obteniendo un valor pointerIndex de la propiedad ActionIndex. Este valor ActionIndex diferencia entre varios dedos, pero no es coherente entre varios eventos. Por ese motivo, se usa pointerIndex para obtener el valor id del puntero del método GetPointerId. Este identificador es coherente en varios eventos:

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;
}

Observe que la invalidación usa la propiedad ActionMasked en la instrucción switch en lugar de la propiedad Action. Aquí se detallan los motivos:

Cuando se trabaja con varios toques, la propiedad Action tiene un valor de MotionEventsAction.Down para que el primer dedo toque la pantalla y, a continuación, valores de Pointer2Down y Pointer3Down cuando el segundo y tercer dedos también tocan la pantalla. Como los dedos cuarto y quinto hacen contacto, la propiedad Action tiene valores numéricos que ni siquiera corresponden a miembros de la enumeración MotionEventsAction. Tendría que examinar los valores de las marcas de bits en los valores para interpretar lo que significan.

De forma similar, cuando los dedos dejan contacto con la pantalla, la propiedad Action tiene valores de Pointer2Up y Pointer3Up para los dedos segundo y tercero, y Up para el primer dedo.

La propiedad ActionMasked toma un número menor de valores porque está pensada para usarse junto con la propiedad ActionIndex para diferenciar entre varios dedos. Cuando los dedos tocan la pantalla, la propiedad solo puede ser igual a MotionEventActions.Down para el primer dedo y a PointerDown para los dedos siguientes. A medida que los dedos dejan la pantalla, ActionMasked tiene valores de Pointer1Up para los dedos siguientes y Up para el primer dedo.

Al usar ActionMasked, ActionIndex distingue entre los dedos siguientes para tocar y dejar la pantalla, pero normalmente no es necesario usar ese valor excepto como argumento para otros métodos del objeto MotionEvent. Para el seguimiento multitáctil, uno de los métodos más importantes es GetPointerId, que se llama en el código anterior. Ese método devuelve un valor que se puede usar para una clave de diccionario para asociar eventos concretos a los dedos.

La invalidación de OnTouchEvent en el ejemplo procesa los eventos MotionEventActions.Down y PointerDown de forma idéntica mediante la creación de un nuevo objeto FingerPaintPolyline y su adición al diccionario:

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;
        // ...
    }
    // ...        
}

Observe que pointerIndex también se usa para obtener la posición del dedo dentro de la vista. Toda la información táctil está asociada al valor pointerIndex. El elemento id identifica de forma única los dedos entre varios mensajes, de modo que se usa para crear la entrada del diccionario.

Del mismo modo, la invalidación de OnTouchEvent también controla a MotionEventActions.Up y a Pointer1Up de forma idéntica mediante la transferencia de la polilínea completada a la colección completedPolylines para que se puedan dibujar durante la invalidación de OnDraw. El código también quita la entrada id del diccionario:

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;
    }
    // ...        
}

Ahora viene la parte complicada.

Entre los eventos down y up, generalmente hay muchos eventos MotionEventActions.Move. Estos se agrupan en una sola llamada a OnTouchEvent y se deben controlar de forma diferente a los eventos Down y Up. El valor pointerIndex obtenido anteriormente de la propiedad ActionIndex debe omitirse. En su lugar, el método debe obtener varios valores pointerIndex mediante el bucle entre 0 y la propiedad PointerCount y, a continuación, obtener un id para cada uno de esos valores pointerIndex:

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;
        // ...
    }
    // ...        
}

Este tipo de procesamiento permite al ejemplo realizar un seguimiento de los dedos individuales y dibujar los resultados en la pantalla:

Captura de pantalla de ejemplo del ejemplo de FingerPaint

Ya vio cómo se puede realizar un seguimiento de los dedos de forma individual en la pantalla y distinguirlos.