Invocación de eventos desde efectos
Un efecto puede definir e invocar un evento, señalando cambios en la vista nativa subyacente. En este artículo se muestra cómo implementar seguimiento multitáctil de bajo nivel y cómo se generan eventos que indican actividad táctil.
El efecto que se describe en este artículo proporciona acceso a eventos de función táctil de bajo nivel. Estos eventos de bajo nivel no están disponibles a través de las clases GestureRecognizer
existentes, pero son vitales para algunos tipos de aplicaciones. Por ejemplo, una aplicación de dibujo táctil necesita realizar un seguimiento de los dedos individuales cuando se mueven en la pantalla. Un teclado musical debe detectar cuándo se pulsan y sueltan teclas individuales, así como un deslizamiento de dedos de una clave a otra en un glissando.
Un efecto es ideal para el seguimiento multitáctil, ya que puede asociarse a cualquier elemento de Xamarin.Forms.
Eventos táctiles de plataforma
iOS, Android y la Plataforma universal de Windows incluyen una API de bajo nivel que permite a las aplicaciones detectar la actividad táctil. Todas estas plataformas distinguen entre tres tipos básicos de eventos táctiles:
- Presionado, cuando un dedo toca la pantalla
- Movido, cuando se mueve un dedo tocando la pantalla
- Soltado, cuando se separa el dedo de la pantalla
En un entorno multitáctil, varios dedos pueden tocar la pantalla al mismo tiempo. Las diferentes plataformas incluyen un número de identificación (ID) que las aplicaciones pueden usar para distinguir entre varios dedos.
En iOS, la clase UIView
define tres métodos reemplazables, TouchesBegan
, TouchesMoved
y TouchesEnded
correspondientes a estos tres eventos básicos. En el artículo Multi-Touch Finger Tracking (Seguimiento multitáctil) se describe cómo usar estos métodos. Sin embargo, un programa de iOS no necesita invalidar una clase que derive de UIView
para usar estos métodos. UIGestureRecognizer
de iOS también define estos mismos tres métodos y se puede adjuntar a una instancia de una clase que deriva de UIGestureRecognizer
a cualquier objeto UIView
.
En Android, la clase View
define un método reemplazable denominado OnTouchEvent
para procesar toda la actividad táctil. El tipo de la actividad táctil se define por miembros de la enumeración Down
, PointerDown
, Move
, Up
y PointerUp
tal como se describe en el artículo Multi-Touch Finger Tracking (Seguimiento multitáctil). View
de Android también define un evento denominado Touch
que permite que un controlador de eventos se adjunte a cualquier objeto View
.
En la Plataforma universal de Windows (UWP), la clase UIElement
define eventos denominados PointerPressed
, PointerMoved
y PointerReleased
. Estos se describen en el artículo Handle pointer input (Controlar la entrada del puntero) de MSDN y en la documentación de API para la clase UIElement
.
La API Pointer
en la Plataforma universal de Windows está diseñada para unificar la entrada de mouse, táctil y manuscrita. Por ese motivo, el evento PointerMoved
se invoca cuando el mouse se mueve a través de un elemento, incluso cuando no se ha presionado ningún botón del mouse. El objeto PointerRoutedEventArgs
que acompaña a estos eventos tiene una propiedad denominada Pointer
, que tiene una propiedad denominada IsInContact
, que indica si se presiona un botón del mouse o un dedo está en contacto con la pantalla.
Además, la UWP define dos eventos más denominados PointerEntered
y PointerExited
. Estos indican si un dedo o el mouse se mueven de un elemento a otro. Por ejemplo, imagine dos elementos adyacentes denominados A y B. Ambos elementos tienen controladores instalados para los eventos de puntero. Cuando se presiona un dedo en A, el evento PointerPressed
se invoca. Cuando se mueve el dedo, A invoca eventos PointerMoved
. Si el dedo se mueve de A a B, A invoca un evento PointerExited
y B invoca un evento PointerEntered
. Si después se suelta el dedo, B invoca un evento PointerReleased
.
Las plataformas iOS y Android son diferentes de UWP: la vista que obtiene primero la llamada a TouchesBegan
o OnTouchEvent
cuando un dedo toca la vista continúa obteniendo toda la actividad de interacción, incluso si se mueve el dedo a distintas vistas. UWP puede comportarse de forma similar si la aplicación captura el puntero: en el controlador de eventos PointerEntered
, el elemento llama a CapturePointer
y después obtiene toda la actividad táctil de ese dedo.
El enfoque de UWP resulta muy útil para algunos tipos de aplicaciones, por ejemplo, un teclado musical. Cada clave puede controlar los eventos táctiles para esa clave y detectar cuando un dedo se deslizó de una tecla a otra mediante los eventos PointerEntered
y PointerExited
.
Por ese motivo, el efecto de seguimiento táctil que se describe en este artículo implementa el enfoque de UWP.
La API de efecto de seguimiento táctil
El ejemplo contiene las clases (y una enumeración) que implementan el seguimiento táctil de bajo nivel. Estos tipos pertenecen al espacio de nombres TouchTracking
y comienzan con la palabra Touch
. El proyecto de biblioteca de .NET Standard TouchTrackingEffectDemos incluye la enumeración TouchActionType
para el tipo de eventos táctiles:
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
Todas las plataformas incluyen también un evento que indica que se ha cancelado el evento táctil.
La clase TouchEffect
de la biblioteca .NET Standard deriva de RoutingEffect
y define un evento denominado TouchAction
y un método denominado OnTouchAction
que invoca al evento TouchAction
:
public class TouchEffect : RoutingEffect
{
public event TouchActionEventHandler TouchAction;
public TouchEffect() : base("XamarinDocs.TouchEffect")
{
}
public bool Capture { set; get; }
public void OnTouchAction(Element element, TouchActionEventArgs args)
{
TouchAction?.Invoke(element, args);
}
}
Observe también la propiedad Capture
. Para capturar eventos táctiles, una aplicación debe establecer esta propiedad en true
antes de un evento Pressed
. En caso contrario, los eventos táctiles se comportan como los de la Plataforma universal de Windows.
La clase TouchActionEventArgs
en la biblioteca de .NET Standard contiene toda la información que acompaña a cada evento:
public class TouchActionEventArgs : EventArgs
{
public TouchActionEventArgs(long id, TouchActionType type, Point location, bool isInContact)
{
Id = id;
Type = type;
Location = location;
IsInContact = isInContact;
}
public long Id { private set; get; }
public TouchActionType Type { private set; get; }
public Point Location { private set; get; }
public bool IsInContact { private set; get; }
}
Una aplicación puede utilizar la propiedad Id
para el seguimiento de dedos individuales. Observe la propiedad IsInContact
. Esta propiedad es siempre true
para eventos Pressed
y false
para eventos Released
. También es siempre true
para eventos Moved
en iOS y Android. La propiedad IsInContact
podría ser false
para Moved
eventos en la Plataforma universal de Windows cuando se ejecuta el programa en el escritorio y se mueve el puntero del mouse sin un botón presionado.
Puede usar la clase TouchEffect
en sus propias aplicaciones si incluye el archivo en el proyecto de biblioteca de .NET Standard de la solución y si agrega una instancia a la colección de Effects
de cualquier elemento de Xamarin.Forms. Adjunte un controlador al evento TouchAction
para obtener los eventos táctiles.
Para usar TouchEffect
en su propia aplicación, también necesitará las implementaciones de plataforma incluidas en la solución TouchTrackingEffectDemos.
Las implementaciones de efecto de seguimiento táctil
Las implementaciones de iOS, Android y UWP de TouchEffect
se describen aquí comenzando con la implementación más sencilla (UWP) y terminando con la implementación de iOS porque es estructuralmente más compleja que las demás.
La implementación de UWP
La implementación de UWP de TouchEffect
es la más sencilla. Como de costumbre, la clase se deriva de PlatformEffect
e incluye dos atributos de ensamblado:
[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]
namespace TouchTracking.UWP
{
public class TouchEffect : PlatformEffect
{
...
}
}
La invalidación OnAttached
guarda información, como los campos, y adjunta los controladores a todos los eventos de puntero:
public class TouchEffect : PlatformEffect
{
FrameworkElement frameworkElement;
TouchTracking.TouchEffect effect;
Action<Element, TouchActionEventArgs> onTouchAction;
protected override void OnAttached()
{
// Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
frameworkElement = Control == null ? Container : Control;
// Get access to the TouchEffect class in the .NET Standard library
effect = (TouchTracking.TouchEffect)Element.Effects.
FirstOrDefault(e => e is TouchTracking.TouchEffect);
if (effect != null && frameworkElement != null)
{
// Save the method to call on touch events
onTouchAction = effect.OnTouchAction;
// Set event handlers on FrameworkElement
frameworkElement.PointerEntered += OnPointerEntered;
frameworkElement.PointerPressed += OnPointerPressed;
frameworkElement.PointerMoved += OnPointerMoved;
frameworkElement.PointerReleased += OnPointerReleased;
frameworkElement.PointerExited += OnPointerExited;
frameworkElement.PointerCanceled += OnPointerCancelled;
}
}
...
}
El controlador OnPointerPressed
invoca el evento de efecto llamando al campo onTouchAction
en el método CommonHandler
:
public class TouchEffect : PlatformEffect
{
...
void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Pressed, args);
// Check setting of Capture property
if (effect.Capture)
{
(sender as FrameworkElement).CapturePointer(args.Pointer);
}
}
...
void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
{
PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
Windows.Foundation.Point windowsPoint = pointerPoint.Position;
onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
touchActionType,
new Point(windowsPoint.X, windowsPoint.Y),
args.Pointer.IsInContact));
}
}
OnPointerPressed
también comprueba el valor de la propiedad Capture
en la clase efecto en la biblioteca de .NET Standard y llama a CapturePointer
si es true
.
Los otros controladores de eventos de UWP son incluso más sencillos:
public class TouchEffect : PlatformEffect
{
...
void OnPointerEntered(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Entered, args);
}
...
}
La implementación de Android
Las implementaciones de Android y iOS son necesariamente más complejas porque deben implementar los eventos Exited
y Entered
cuando un dedo se mueve de un elemento a otro. Ambas implementaciones tienen una estructura similar.
La clase TouchEffect
de Android instala un controlador para el evento Touch
:
view = Control == null ? Container : Control;
...
view.Touch += OnTouch;
La clase también define dos diccionarios estáticos:
public class TouchEffect : PlatformEffect
{
...
static Dictionary<Android.Views.View, TouchEffect> viewDictionary =
new Dictionary<Android.Views.View, TouchEffect>();
static Dictionary<int, TouchEffect> idToEffectDictionary =
new Dictionary<int, TouchEffect>();
...
viewDictionary
obtiene una nueva entrada cada vez que se llama a la invalidación OnAttached
:
viewDictionary.Add(view, this);
La entrada se quita del diccionario en OnDetached
. Todas las instancias de TouchEffect
están asociadas a una vista concreta a la que está conectada el efecto. El diccionario estático permite cualquier instancia de TouchEffect
para enumerar todas las demás vistas y sus correspondientes instancias de TouchEffect
. Esto es necesario para permitir la transferencia de los eventos de una vista a otra.
Android asigna un código de identificador a los eventos táctiles que permite que una aplicación realice un seguimiento de los dedos individuales. El idToEffectDictionary
asocia este código de identificador con una instancia de TouchEffect
. Se agrega un elemento a este diccionario cuando se llama al controlador Touch
para una pulsación de dedo:
void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
...
switch (args.Event.ActionMasked)
{
case MotionEventActions.Down:
case MotionEventActions.PointerDown:
FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);
idToEffectDictionary.Add(id, this);
capture = libTouchEffect.Capture;
break;
El elemento se quita de idToEffectDictionary
cuando se separa el dedo de la pantalla. El método FireEvent
simplemente acumula toda la información necesaria para llamar al método OnTouchAction
:
void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
// Get the method to call for firing events
Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.libTouchEffect.OnTouchAction;
// Get the location of the pointer within the view
touchEffect.view.GetLocationOnScreen(twoIntArray);
double x = pointerLocation.X - twoIntArray[0];
double y = pointerLocation.Y - twoIntArray[1];
Point point = new Point(fromPixels(x), fromPixels(y));
// Call the method
onTouchAction(touchEffect.formsElement,
new TouchActionEventArgs(id, actionType, point, isInContact));
}
Se procesan todos los demás tipos de interacción de dos maneras diferentes: si la propiedad Capture
es true
, el evento táctil es una traducción bastante simple para la información de TouchEffect
. Se complica más cuando Capture
es false
, porque los eventos táctiles quizás tengan que moverse de una vista a otra. Esta es la responsabilidad del método CheckForBoundaryHop
, al que se llama durante los eventos de movimiento. Este método usa ambos diccionarios estáticos. Enumera a través de viewDictionary
para determinar la vista que actualmente esté tocando el dedo y usa idToEffectDictionary
para almacenar la instancia de TouchEffect
actual (y por lo tanto, la vista actual) asociada con un identificador determinado:
void CheckForBoundaryHop(int id, Point pointerLocation)
{
TouchEffect touchEffectHit = null;
foreach (Android.Views.View view in viewDictionary.Keys)
{
// Get the view rectangle
try
{
view.GetLocationOnScreen(twoIntArray);
}
catch // System.ObjectDisposedException: Cannot access a disposed object.
{
continue;
}
Rectangle viewRect = new Rectangle(twoIntArray[0], twoIntArray[1], view.Width, view.Height);
if (viewRect.Contains(pointerLocation))
{
touchEffectHit = viewDictionary[view];
}
}
if (touchEffectHit != idToEffectDictionary[id])
{
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, TouchActionType.Exited, pointerLocation, true);
}
if (touchEffectHit != null)
{
FireEvent(touchEffectHit, id, TouchActionType.Entered, pointerLocation, true);
}
idToEffectDictionary[id] = touchEffectHit;
}
}
Si ha habido un cambio en el idToEffectDictionary
, el método potencialmente llama a FireEvent
para Exited
y Entered
para transferir de una vista a otra. Con todo, es posible que el dedo se moviese a un área ocupada por una vista sin un TouchEffect
adjunto o desde esa área a una vista con el efecto adjunto.
Tenga en cuenta el bloqueo try
y catch
cuando se accede a la vista. En una página a la que se navega que después navega a la página principal, no se llama al método OnDetached
y los elementos permanecen en el viewDictionary
pero Android los considera eliminados.
La implementación de iOS
La implementación de iOS es similar a la implementación de Android, salvo que la clase TouchEffect
de iOS debe crear una instancia de un derivado de UIGestureRecognizer
. Se trata de una clase en el proyecto de iOS denominado TouchRecognizer
. Esta clase mantiene dos diccionarios estáticos que almacenan instancias de TouchRecognizer
:
static Dictionary<UIView, TouchRecognizer> viewDictionary =
new Dictionary<UIView, TouchRecognizer>();
static Dictionary<long, TouchRecognizer> idToTouchDictionary =
new Dictionary<long, TouchRecognizer>();
Gran parte de la estructura de esta clase TouchRecognizer
es similar a la de la clase TouchEffect
de Android.
Importante
Muchas de las vistas de UIKit
no tienen la funcionalidad táctil habilitada de forma predeterminada. La funcionalidad táctil se puede habilitar agregando view.UserInteractionEnabled = true;
a la invalidación OnAttached
en la clase TouchEffect
en el proyecto de iOS. Esto debe ocurrir después obtener UIView
, que se corresponde con el elemento al que está asociado el efecto.
Poner el efecto táctil en funcionamiento
El programa de ejemplo contiene cinco páginas que prueban el efecto de seguimiento táctil para tareas comunes.
La página Arrastre de BoxView le permite agregar elementos BoxView
a un AbsoluteLayout
y después arrastrarlos en la pantalla. El archivo XAML crea instancias de dos vistas de Button
para agregar elementos BoxView
a la AbsoluteLayout
y borrar el AbsoluteLayout
.
El método en el archivo de código subyacente que agrega un nuevo BoxView
a AbsoluteLayout
también agrega un objeto TouchEffect
para BoxView
y adjunta un controlador de eventos para el efecto:
void AddBoxViewToLayout()
{
BoxView boxView = new BoxView
{
WidthRequest = 100,
HeightRequest = 100,
Color = new Color(random.NextDouble(),
random.NextDouble(),
random.NextDouble())
};
TouchEffect touchEffect = new TouchEffect();
touchEffect.TouchAction += OnTouchEffectAction;
boxView.Effects.Add(touchEffect);
absoluteLayout.Children.Add(boxView);
}
El controlador de eventos TouchAction
procesa todos los eventos táctiles para todos los elementos de BoxView
, pero debe usarse con cuidado: no puede permitir dos dedos en una sola BoxView
porque el programa solo implementa el arrastre y los dos dedos podrían interferir entre sí. Por este motivo, la página define una clase incrustada para cada dedo del que está realizando el seguimiento:
class DragInfo
{
public DragInfo(long id, Point pressPoint)
{
Id = id;
PressPoint = pressPoint;
}
public long Id { private set; get; }
public Point PressPoint { private set; get; }
}
Dictionary<BoxView, DragInfo> dragDictionary = new Dictionary<BoxView, DragInfo>();
El dragDictionary
contiene una entrada para cada BoxView
que se arrastra actualmente.
La acción táctil Pressed
agrega un elemento a este diccionario y la acción Released
lo quita. La lógica Pressed
debe comprobar si ya hay un elemento en el diccionario para esa BoxView
. Si es así, BoxView
ya se está arrastrando y el nuevo evento es un segundo dedo en esa misma BoxView
. Para las acciones Moved
y Released
, el controlador de eventos debe comprobar si el diccionario tiene una entrada para esa BoxView
y que la propiedad Id
táctil para la BoxView
arrastrada coincide con el que aparece en la entrada del diccionario:
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
BoxView boxView = sender as BoxView;
switch (args.Type)
{
case TouchActionType.Pressed:
// Don't allow a second touch on an already touched BoxView
if (!dragDictionary.ContainsKey(boxView))
{
dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));
// Set Capture property to true
TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
touchEffect.Capture = true;
}
break;
case TouchActionType.Moved:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
Point initialLocation = dragDictionary[boxView].PressPoint;
rect.X += args.Location.X - initialLocation.X;
rect.Y += args.Location.Y - initialLocation.Y;
AbsoluteLayout.SetLayoutBounds(boxView, rect);
}
break;
case TouchActionType.Released:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
dragDictionary.Remove(boxView);
}
break;
}
}
La lógica Pressed
establece la propiedad Capture
del objeto TouchEffect
en true
. Esto tiene el efecto de entregar todos los eventos subsiguientes para ese dedo en el mismo controlador de eventos.
La lógica Moved
mueve la BoxView
modificando la propiedad adjunta LayoutBounds
. La propiedad Location
de los argumentos de evento siempre es relativa a la BoxView
que se está arrastrando y si el BoxView
se está arrastrando a una velocidad constante, las propiedades Location
de los eventos consecutivos serán aproximadamente las mismas. Por ejemplo, si un dedo presiona el BoxView
en su centro, la acción Pressed
almacena una propiedad PressPoint
de (50, 50), que sigue siendo la misma para los eventos posteriores. Si la BoxView
se arrastra en diagonal a una velocidad constante, las propiedades Location
subsiguientes durante la acción Moved
podrían ser valores de (55, 55), en cuyo caso la lógica Moved
agrega 5 a la posición horizontal y vertical de la BoxView
. Esto mueve la BoxView
de forma que su centro está de nuevo directamente bajo el dedo.
Puede mover varios elementos BoxView
al mismo tiempo usando diferentes dedos.
Creación de subclases de la vista
A menudo, resulta más fácil para un elemento de Xamarin.Forms controlar sus propios eventos táctiles. La página Arrastre de BoxView arrastrable funciona igual que la página Arrastre de BoxView, pero los elementos que el usuario arrastra son instancias de una clase DraggableBoxView
que se deriva de BoxView
:
class DraggableBoxView : BoxView
{
bool isBeingDragged;
long touchId;
Point pressPoint;
public DraggableBoxView()
{
TouchEffect touchEffect = new TouchEffect
{
Capture = true
};
touchEffect.TouchAction += OnTouchEffectAction;
Effects.Add(touchEffect);
}
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
if (!isBeingDragged)
{
isBeingDragged = true;
touchId = args.Id;
pressPoint = args.Location;
}
break;
case TouchActionType.Moved:
if (isBeingDragged && touchId == args.Id)
{
TranslationX += args.Location.X - pressPoint.X;
TranslationY += args.Location.Y - pressPoint.Y;
}
break;
case TouchActionType.Released:
if (isBeingDragged && touchId == args.Id)
{
isBeingDragged = false;
}
break;
}
}
}
El constructor crea y adjunta el TouchEffect
y establece la propiedad Capture
cuando se crea una instancia de ese objeto por primera vez. No se necesita ningún diccionario porque la propia clase almacena valores isBeingDragged
, pressPoint
y touchId
asociados con cada dedo. El control Moved
modifica las propiedades TranslationX
y TranslationY
, por lo que la lógica funcionará incluso si el elemento primario de la DraggableBoxView
no es un AbsoluteLayout
.
Integración con SkiaSharp
Las siguientes dos demostraciones requieren gráficos y usan SkiaSharp para este propósito. Es posible que le interese obtener información sobre el uso de SkiaSharp en Xamarin.Forms antes de estudiar estos ejemplos. Los dos primeros artículos ("Conceptos básicos de dibujo de SkiaSharp" y "Rutas y líneas de acceso de SkiaSharp") incluyen todo lo que necesitará aquí.
La página Ellipse Drawing (Dibujo de elipse) le permite dibujar una elipse deslizando el dedo en la pantalla. Dependiendo de cómo mueva el dedo, puede dibujar la elipse desde la esquina superior izquierda a la esquina inferior derecha, o desde cualquier otra esquina hasta la esquina opuesta. La elipse se dibuja con un color aleatorio y una opacidad.
Si después toca uno de los puntos suspensivos, puede arrastrarla a otra ubicación. Esto requiere una técnica conocida como "prueba de posicionamiento," lo que implica buscar el objeto gráfico en un momento determinado. Los puntos suspensivos de SkiaSharp no son elementos de Xamarin.Forms, por lo que no pueden realizar su propio procesamiento de TouchEffect
. El TouchEffect
debe aplicarse a todo el objeto SKCanvasView
.
El archivo EllipseDrawPage.xaml crea una instancia de SKCanvasView
en un Grid
de una sola celda. El objeto TouchEffect
se asocia a ese Grid
:
<Grid x:Name="canvasViewGrid"
Grid.Row="1"
BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
En la Plataforma universal de Windows y en Android el TouchEffect
se puede conectar directamente a la SKCanvasView
, pero en iOS no es posible. Tenga en cuenta que la propiedad Capture
está establecida en true
.
Cada elipse que representa SkiaSharp se representa mediante un objeto de tipo EllipseDrawingFigure
:
class EllipseDrawingFigure
{
SKPoint pt1, pt2;
public EllipseDrawingFigure()
{
}
public SKColor Color { set; get; }
public SKPoint StartPoint
{
set
{
pt1 = value;
MakeRectangle();
}
}
public SKPoint EndPoint
{
set
{
pt2 = value;
MakeRectangle();
}
}
void MakeRectangle()
{
Rectangle = new SKRect(pt1.X, pt1.Y, pt2.X, pt2.Y).Standardized;
}
public SKRect Rectangle { set; get; }
// For dragging operations
public Point LastFingerLocation { set; get; }
// For the dragging hit-test
public bool IsInEllipse(SKPoint pt)
{
SKRect rect = Rectangle;
return (Math.Pow(pt.X - rect.MidX, 2) / Math.Pow(rect.Width / 2, 2) +
Math.Pow(pt.Y - rect.MidY, 2) / Math.Pow(rect.Height / 2, 2)) < 1;
}
}
Las propiedades StartPoint
y EndPoint
se utilizan cuando el programa está procesando la entrada táctil; la propiedad Rectangle
se utiliza para dibujar la elipse. La propiedad LastFingerLocation
entra en juego cuando se arrastra la elipse y el método IsInEllipse
ayuda en la prueba de posicionamiento. El método devuelve true
si el punto está dentro de la elipse.
El archivo de código subyacente mantiene tres colecciones:
Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();
El diccionario draggingFigure
contiene un subconjunto de la colección de completedFigures
. El controlador de eventos PaintSurface
de SkiaSharp representa simplemente los objetos en las colecciones completedFigures
y inProgressFigures
:
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Fill
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (EllipseDrawingFigure figure in completedFigures)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
foreach (EllipseDrawingFigure figure in inProgressFigures.Values)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
}
La parte más complicada del procesamiento táctil es el control Pressed
. Aquí es donde se realiza la prueba de posicionamiento, pero si el código detecta una elipse bajo el dedo del usuario, dicha elipse solo se puede arrastrar si actualmente no la está arrastrando otro dedo. Si no hay ninguna elipse bajo el dedo del usuario, el código comienza el proceso de dibujar una elipse nueva:
case TouchActionType.Pressed:
bool isDragOperation = false;
// Loop through the completed figures
foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>())
{
// Check if the finger is touching one of the ellipses
if (fig.IsInEllipse(ConvertToPixel(args.Location)))
{
// Tentatively assume this is a dragging operation
isDragOperation = true;
// Loop through all the figures currently being dragged
foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values)
{
// If there's a match, we'll need to dig deeper
if (fig == draggedFigure)
{
isDragOperation = false;
break;
}
}
if (isDragOperation)
{
fig.LastFingerLocation = args.Location;
draggingFigures.Add(args.Id, fig);
break;
}
}
}
if (isDragOperation)
{
// Move the dragged ellipse to the end of completedFigures so it's drawn on top
EllipseDrawingFigure fig = draggingFigures[args.Id];
completedFigures.Remove(fig);
completedFigures.Add(fig);
}
else // start making a new ellipse
{
// Random bytes for random color
byte[] buffer = new byte[4];
random.NextBytes(buffer);
EllipseDrawingFigure figure = new EllipseDrawingFigure
{
Color = new SKColor(buffer[0], buffer[1], buffer[2], buffer[3]),
StartPoint = ConvertToPixel(args.Location),
EndPoint = ConvertToPixel(args.Location)
};
inProgressFigures.Add(args.Id, figure);
}
canvasView.InvalidateSurface();
break;
Otro ejemplo de SkiaSharp es la página Finger Paint (Dibujo con los dedos). Puede seleccionar un color de trazo y el ancho del trazo desde dos vistas Picker
y después dibujar con uno o más dedos:
Este ejemplo también requiere una clase independiente para representar cada línea dibujada en la pantalla:
class FingerPaintPolyline
{
public FingerPaintPolyline()
{
Path = new SKPath();
}
public SKPath Path { set; get; }
public Color StrokeColor { set; get; }
public float StrokeWidth { set; get; }
}
Un objeto SKPath
se usa para representar cada línea. El archivo FingerPaint.xaml.cs mantiene dos colecciones de estos objetos, una para aquellas polilíneas que se están dibujando actualmente y otra para las polilíneas completadas:
Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();
El procesamiento de Pressed
crea una nueva FingerPaintPolyline
, llama a MoveTo
en el objeto de ruta de acceso para almacenar el punto inicial y agrega ese objeto al diccionario de inProgressPolylines
. El procesamiento de Moved
llama a LineTo
en el objeto de ruta con la nueva posición del dedo y el procesamiento de Released
transfiere la polilínea completada desde inProgressPolylines
a completedPolylines
. Una vez más, el código de dibujo de SkiaSharp real es relativamente sencillo:
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (FingerPaintPolyline polyline in completedPolylines)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
}
Seguimiento táctil de vista a vista
Todos los ejemplos anteriores han establecido la propiedad Capture
del TouchEffect
en true
, ya sea cuando el TouchEffect
se creó o cuando se produjo el evento Pressed
. Esto garantiza que el mismo elemento recibe todos los eventos asociados con el dedo que presionó la vista primero. El ejemplo final no establece Capture
en true
. Esto provoca un comportamiento diferente cuando se mueve un dedo en contacto con la pantalla de un elemento a otro. El elemento desde el que se mueve el dedo recibe un evento con una propiedad Type
establecida en TouchActionType.Exited
y el segundo elemento recibe un evento con una configuración Type
de TouchActionType.Entered
.
Este tipo de procesamiento táctil es muy útil para un teclado de música. Una tecla debe ser capaz de detectar cuándo se pulsa, pero también cuándo un dedo pasa de una tecla a otra.
La página Silent Keyboard teclado silencioso define clases WhiteKey
y BlackKey
pequeñas que derivan de Key
, que se deriva de BoxView
.
La clase Key
está lista para usarse en un programa de música real. Define propiedades públicas denominadas IsPressed
y KeyNumber
, que están pensadas para establecerse en el código de teclas que establece el estándar MIDI. La clase Key
también define un evento denominado StatusChanged
, que se invoca cuando la propiedad IsPressed
cambia.
Se permiten varios dedos en cada tecla. Por este motivo, la clase Key
mantiene una List
de los números de Id. táctil de todos los dedos que tocan actualmente esa tecla:
List<long> ids = new List<long>();
El controlador de eventos TouchAction
agrega un identificador para la lista ids
para un tipo de evento Pressed
y un tipo Entered
, pero solo cuando la propiedad IsInContact
es true
para el evento Entered
. Se quita el identificador de la List
para un evento Released
o Exited
:
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
AddToList(args.Id);
break;
case TouchActionType.Entered:
if (args.IsInContact)
{
AddToList(args.Id);
}
break;
case TouchActionType.Moved:
break;
case TouchActionType.Released:
case TouchActionType.Exited:
RemoveFromList(args.Id);
break;
}
}
Los métodos AddToList
y RemoveFromList
comprueban si la List
ha cambiado entre vacía y no vacía y si es así, invocan el evento StatusChanged
.
Los distintos elementos WhiteKey
y BlackKey
se organizan en el archivo XAML de la página, que tiene mejor aspecto cuando se mantiene el teléfono en un modo horizontal:
Si pasa los dedos por las teclas, podrá ver por los pequeños cambios en el color que los eventos táctiles se transfieren de una tecla a otra.
Resumen
En este artículo se mostró cómo invocar eventos en un efecto, y cómo escribir y usar un efecto que implementa el procesamiento multitáctil de bajo nivel.
Vínculos relacionados
- Multi-Touch Finger Tracking in iOS (Seguimiento de dedos multitáctil en iOS)
- Multi-Touch Finger Tracking in Android (Seguimiento de dedos multitáctil en Android)