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:
Ya vio cómo se puede realizar un seguimiento de los dedos de forma individual en la pantalla y distinguirlos.