Seguimiento de dedos multitáctil en Xamarin.iOS

En este documento se muestra cómo realizar un seguimiento de eventos táctiles con 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 procese varios eventos táctiles, deberá distinguir entre estos dedos.

Cuando un dedo toque por primera vez la pantalla, iOS creará un objeto UITouch para ese dedo. Este objeto será el mismo que el del movimiento del dedo en la pantalla y, a continuación, se elevará de la pantalla, en cuyo punto se eliminará el objeto. Para realizar un seguimiento de los dedos, un programa debería evitar almacenar este objeto UITouch directamente. En su lugar, puede usar la propiedad Handle de tipo IntPtr para identificar de forma única estos objetos UITouch.

Casi siempre, un programa que realiza un seguimiento de los dedos de forma individual mantiene un diccionario para el seguimiento táctil. Para un programa de iOS, la clave de diccionario es el valor Handle que identifica un dedo determinado. El valor del diccionario dependerá de la aplicación. En el programa FingerPaint, cada trazo de dedo (de entrada táctil a 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 CGPath();
    }

    public CGColor Color { set; get; }

    public float StrokeWidth { set; get; }

    public CGPath Path { private set; get; }
}

Cada Polilínea tiene un color, un ancho de trazo y un objeto CGPath gráfico de iOS 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 derivado UIView denominado FingerPaintCanvasView. Esa clase mantiene un diccionario de objetos de tipo FingerPaintPolyline durante el tiempo en que uno o varios dedos dibujan activamente:

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

Este diccionario permite la vista para obtener rápidamente la información FingerPaintPolyline asociada a cada dedo en función de la propiedad Handle del objeto UITouch.

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 cinco métodos definidos por View:

Las distintas invalidaciones de Touches acumulan los puntos que componen las Polilíneas.

La invalidación [Draw] dibuja las Polilíneas completadas y, a continuación, las Polilíneas en curso:

public override void Draw(CGRect rect)
{
    base.Draw(rect);

    using (CGContext context = UIGraphics.GetCurrentContext())
    {
        // Stroke settings
        context.SetLineCap(CGLineCap.Round);
        context.SetLineJoin(CGLineJoin.Round);

        // Draw the completed polylines
        foreach (FingerPaintPolyline polyline in completedPolylines)
        {
            context.SetStrokeColor(polyline.Color);
            context.SetLineWidth(polyline.StrokeWidth);
            context.AddPath(polyline.Path);
            context.DrawPath(CGPathDrawingMode.Stroke);
        }

        // Draw the in-progress polylines
        foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
        {
            context.SetStrokeColor(polyline.Color);
            context.SetLineWidth(polyline.StrokeWidth);
            context.AddPath(polyline.Path);
            context.DrawPath(CGPathDrawingMode.Stroke);
        }
    }
}

Cada una de las invalidaciones Touches informa potencialmente de las acciones de varios dedos, indicadas por uno o varios objetos UITouch almacenados en el argumento touches al método. Las invalidaciones de TouchesBegan recorren en bucle estos objetos. Para cada objeto UITouch, el método creará e inicializará un nuevo objeto FingerPaintPolyline, incluyendo el almacenamiento de la ubicación inicial del dedo obtenido del método LocationInView. Este objeto FingerPaintPolyline se agregará al diccionario InProgressPolylines mediante la propiedad Handle del objeto UITouch como clave de diccionario:

public override void TouchesBegan(NSSet touches, UIEvent evt)
{
    base.TouchesBegan(touches, evt);

    foreach (UITouch touch in touches.Cast<UITouch>())
    {
        // Create a FingerPaintPolyline, set the initial point, and store it
        FingerPaintPolyline polyline = new FingerPaintPolyline
        {
            Color = StrokeColor,
            StrokeWidth = StrokeWidth,
        };

        polyline.Path.MoveToPoint(touch.LocationInView(this));
        inProgressPolylines.Add(touch.Handle, polyline);
    }
    SetNeedsDisplay();
}

El método concluye llamando a SetNeedsDisplay a para generar una llamada a la invalidación Draw y para actualizar la pantalla.

A medida que el dedo o los dedos se mueven en la pantalla, View obtiene varias llamadas a su invalidación TouchesMoved. Esta invalidación recorrerá de forma similar los objetos UITouch almacenados en el argumento touches y agregará la ubicación actual del dedo a la ruta de acceso de grafo:

public override void TouchesMoved(NSSet touches, UIEvent evt)
{
    base.TouchesMoved(touches, evt);

    foreach (UITouch touch in touches.Cast<UITouch>())
    {
        // Add point to path
        inProgressPolylines[touch.Handle].Path.AddLineToPoint(touch.LocationInView(this));
    }
    SetNeedsDisplay();
}

La colección touches contiene solo los objetos UITouch de los dedos que se movieron desde la última llamada a TouchesBegan o TouchesMoved. Si alguna vez necesitase objetos UITouch correspondientes a todos los dedos en contacto con la pantalla en ese momento, esa información estará disponible a través de la propiedad AllTouches del argumento UIEvent para el método.

La invalidación TouchesEnded tiene dos trabajos. Debe agregar el último punto a la ruta de acceso de grafos y transferir el objeto FingerPaintPolyline del diccionario inProgressPolylines a la lista completedPolylines:

public override void TouchesEnded(NSSet touches, UIEvent evt)
{
    base.TouchesEnded(touches, evt);

    foreach (UITouch touch in touches.Cast<UITouch>())
    {
        // Get polyline from dictionary and remove it from dictionary
        FingerPaintPolyline polyline = inProgressPolylines[touch.Handle];
        inProgressPolylines.Remove(touch.Handle);

        // Add final point to path and save with completed polylines
        polyline.Path.AddLineToPoint(touch.LocationInView(this));
        completedPolylines.Add(polyline);
    }
    SetNeedsDisplay();
}

La invalidación TouchesCancelled se controla simplemente abandonando el objeto FingerPaintPolyline en el diccionario:

public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
    base.TouchesCancelled(touches, evt);

    foreach (UITouch touch in touches.Cast<UITouch>())
    {
        inProgressPolylines.Remove(touch.Handle);
    }
    SetNeedsDisplay();
}

De forma completa, este procesamiento permite al programa FingerPaint realizar un seguimiento de los dedos de forma individual y dibujar los resultados en la pantalla:

Tracking individual fingers and drawing the results on the screen

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