Invocando eventos por meio de efeitos
Um efeito pode definir e invocar um evento, sinalizando as alterações na exibição nativa subjacente. Este artigo mostra como implementar o acompanhamento de dedos multitoque de nível baixo e como gerar eventos que sinalizam a atividade de toque.
O efeito descrito neste artigo fornece acesso aos eventos de toque de nível baixo. Esses eventos de nível baixo não estão disponíveis por meio das classes GestureRecognizer
existentes, mas elas são vitais para alguns tipos de aplicativos. Por exemplo, um aplicativo de pintura a dedo precisa acompanhar dedos individuais à medida que eles se movem na tela. Um teclado musical precisa detectar toques e liberações em teclas individuais, bem como o deslize de um dedo de uma tecla a outra em um glissando.
Um efeito é ideal para rastreamento de dedos multitoque porque pode ser anexado a qualquer Xamarin.Forms elemento.
Eventos de toque da plataforma
O iOS, o Android e a Plataforma Universal do Windows incluem uma API de nível baixo que permite que os aplicativos detectem a atividade de toque. Essas plataformas todas distinguem entre três tipos básicos de eventos de toque:
- Pressionado, quando um dedo toca a tela
- Movido, quando um dedo que toca a tela se move
- Liberado, quando o dedo é liberado da tela
Em um ambiente multitoque, vários dedos podem tocar a tela ao mesmo tempo. As várias plataformas incluem um número de ID (identificação) que os aplicativos podem usar para distinguir entre vários dedos.
No iOS, a classe UIView
define três métodos substituíveis, TouchesBegan
, TouchesMoved
e TouchesEnded
, correspondentes a esses três eventos básicos. O artigo Acompanhamento de dedos multitoque descreve como usar esses métodos. No entanto, um programa do iOS não precisa substituir uma classe que deriva de UIView
para usar esses métodos. O UIGestureRecognizer
do iOS também define esses mesmos três métodos, sendo que você pode anexar uma instância de uma classe que deriva de UIGestureRecognizer
a qualquer objeto UIView
.
No Android, a classe View
define um método substituível chamado OnTouchEvent
para processar todas as atividades de toque. O tipo da atividade de toque é definido pelos membros de enumeração Down
, PointerDown
, Move
, Up
e PointerUp
, conforme descrito no artigo Acompanhamento de dedos multitoque. O View
do Android também define um evento chamado Touch
que permite que um manipulador de eventos seja anexado a qualquer objeto View
.
No UWP (Plataforma Universal do Windows), a classe UIElement
define eventos chamados PointerPressed
, PointerMoved
e PointerReleased
. Eles são descritos no artigo Manipular a entrada de ponteiro no MSDN e na documentação da API para a classe UIElement
.
A API do Pointer
na Plataforma Universal do Windows se destina a unificar o mouse, o toque e a entrada à caneta. Por esse motivo, o evento PointerMoved
é invocado quando o cursor do mouse passa sobre um elemento, mesmo que o botão do mouse não seja pressionado. O objeto PointerRoutedEventArgs
que acompanha esses eventos tem uma propriedade chamada Pointer
que tem uma propriedade chamada IsInContact
, indicando se um botão do mouse é pressionado ou um dedo está em contato com a tela.
Além disso, o UWP define mais dois eventos chamados PointerEntered
e PointerExited
. Eles indicam quando um mouse ou o dedo e move de um elemento para outro. Por exemplo, considere dois elementos adjacentes, chamados A e B. Ambos os elementos têm manipuladores instalados para os eventos de ponteiro. Quando um dedo pressiona A, o evento PointerPressed
é invocado. Conforme o dedo se move, A invoca eventos PointerMoved
. Se o dedo se move de A para B, A invoca um evento PointerExited
e B invoca um evento PointerEntered
. Se o dedo é então liberado, B invoca um evento PointerReleased
.
As plataformas iOS e Android são diferentes do UWP: a exibição que primeiro obtém a chamada a TouchesBegan
ou OnTouchEvent
quando um dedo toca a exibição continua obtendo todas as atividades de toque, mesmo se o dedo se move para diferentes exibições. O UWP pode se comportar da mesma forma se o aplicativo captura o ponteiro: no manipulador de eventos PointerEntered
, o elemento chama CapturePointer
e, em seguida, obtém todas as atividades de toque desse dedo.
A abordagem do UWP prova ser muito útil para alguns tipos de aplicativos, por exemplo, um teclado musical. Cada tecla pode manipular os eventos de toque dessa tecla e detectar quando um dedo deslizou de uma tecla para outra usando os eventos PointerEntered
e PointerExited
.
Por esse motivo, o efeito do acompanhamento de toque descrito neste artigo implementa a abordagem do UWP.
A API de efeito do acompanhamento de toque
O exemplo contém as classes (e uma enumeração) que implementam o controle de toque de baixo nível. Esses tipos pertencem ao namespace TouchTracking
e começam com a palavra Touch
. O projeto TouchTrackingEffectDemos da biblioteca do .NET Standard inclui a enumeração TouchActionType
para o tipo de eventos de toque:
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
Todas as plataformas também incluem um evento que indica que o evento de toque foi cancelado.
A classe TouchEffect
na biblioteca do .NET Standard deriva de RoutingEffect
e define um evento chamado TouchAction
e um método chamado OnTouchAction
que invoca o 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 também a propriedade Capture
. Para capturar eventos de toque, um aplicativo precisa definir essa propriedade como true
anterior a um evento Pressed
. Caso contrário, os eventos de toque se comportam como aqueles mostrados na Plataforma Universal do Windows.
A classe TouchActionEventArgs
na biblioteca do .NET Standard contém todas as informações que acompanham 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; }
}
Um aplicativo pode usar a propriedade Id
para acompanhar dedos individuais. Observe a propriedade IsInContact
. Essa propriedade é sempre true
para eventos Pressed
e false
para eventos Released
. Ela também é sempre true
para eventos Moved
no iOS e no Android. A propriedade IsInContact
pode ser false
para eventos Moved
na Plataforma Universal do Windows quando o programa está em execução na área de trabalho e o ponteiro do mouse se move sem um botão pressionado.
Você pode usar a TouchEffect
classe em seus próprios aplicativos incluindo o arquivo no projeto de biblioteca .NET Standard da solução e adicionando uma instância à Effects
coleção de qualquer Xamarin.Forms elemento. Anexe um manipulador ao evento TouchAction
para obter os eventos de toque.
Para usar TouchEffect
em seu próprio aplicativo, você também precisará das implementações de plataforma incluídas na solução TouchTrackingEffectDemos.
As implementações do efeito de acompanhamento de toque
As implementações do iOS, do Android e do UWP do TouchEffect
são descritas abaixo começando com a implementação mais simples (UWP) e terminando com a implementação do iOS, porque ela é estruturalmente mais complexa do que as outras.
A implementação do UWP
A implementação do UWP do TouchEffect
é a mais simples. Como de costume, a classe deriva de PlatformEffect
e inclui dois atributos de assembly:
[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]
namespace TouchTracking.UWP
{
public class TouchEffect : PlatformEffect
{
...
}
}
A substituição OnAttached
salva algumas informações como campos e anexa manipuladores a todos os eventos de ponteiro:
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;
}
}
...
}
O manipulador OnPointerPressed
invoca o evento de efeito chamando o campo onTouchAction
no 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
também verifica o valor da propriedade Capture
na classe de efeito na biblioteca do .NET Standard e chama CapturePointer
se ele é true
.
Os outros manipuladores de eventos do UWP são ainda mais simples:
public class TouchEffect : PlatformEffect
{
...
void OnPointerEntered(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Entered, args);
}
...
}
A implementação do Android
As implementações do Android e do iOS são necessariamente mais complexas, porque precisam implementar os eventos Exited
e Entered
quando um dedo se move de um elemento para outro. Ambas as implementações são estruturadas de forma semelhante.
A classe TouchEffect
do Android instala um manipulador para o evento Touch
:
view = Control == null ? Container : Control;
...
view.Touch += OnTouch;
A classe também define dois dicionários 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>();
...
O viewDictionary
obtém uma nova entrada sempre que a substituição OnAttached
é chamada:
viewDictionary.Add(view, this);
A entrada é removida do dicionário em OnDetached
. Todas as instâncias de TouchEffect
são associadas a uma exibição específica à qual o efeito é anexado. O dicionário estático permite que qualquer instância TouchEffect
enumere todas as outras exibições e suas instâncias TouchEffect
correspondentes. Isso é necessário para permitir a transferência dos eventos de uma exibição para outra.
O Android atribui um código de ID a eventos de toque, que permite que um aplicativo acompanhe dedos individuais. O idToEffectDictionary
associa esse código de ID a uma instância TouchEffect
. Um item é adicionado a esse dicionário quando o manipulador Touch
é chamado para um pressionamento 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;
O item é removido do idToEffectDictionary
quando o dedo é liberado da tela. O método FireEvent
apenas acumula todas as informações necessárias para chamar o 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));
}
Todos os outros tipos de toque são processados de duas maneiras diferentes: se a propriedade Capture
é true
, o evento de toque é uma tradução muito simples das informações do TouchEffect
. Isso fica mais complicado quando Capture
é false
porque os eventos de toque talvez precisem ser movidos de uma exibição para outra. Essa é a responsabilidade do método CheckForBoundaryHop
, que é chamado durante eventos de movimentação. Esse método faz uso de ambos os dicionários estáticos. Ele enumera pelo viewDictionary
para determinar a exibição que o dedo está tocando no momento e, em seguida, ele usa idToEffectDictionary
para armazenar a instância TouchEffect
atual (e, portanto, a exibição atual) associado a uma ID específica:
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;
}
}
Se há uma alteração no idToEffectDictionary
, o método potencialmente chama FireEvent
para Exited
e Entered
para a transferência de uma exibição para outra. No entanto, o dedo pode ter sido movido para uma área ocupada por uma exibição sem um TouchEffect
anexado ou dessa área para uma exibição com o efeito anexado.
Observe que o try
e o catch
são bloqueados quando a exibição é acessada. Em uma página na qual o usuário navega e, em seguida, navega novamente para a home page, o método OnDetached
não é chamado e os itens permanecem no viewDictionary
, mas o Android os considera descartados.
A implementação do iOS
A implementação do iOS é semelhante à implementação do Android, exceto que a classe TouchEffect
do iOS precisa criar uma instância de um derivado do UIGestureRecognizer
. Essa é uma classe no projeto do iOS chamada TouchRecognizer
. Essa classe mantém dois dicionários estáticos que armazenam instâncias TouchRecognizer
:
static Dictionary<UIView, TouchRecognizer> viewDictionary =
new Dictionary<UIView, TouchRecognizer>();
static Dictionary<long, TouchRecognizer> idToTouchDictionary =
new Dictionary<long, TouchRecognizer>();
Grande parte da estrutura desta classe TouchRecognizer
é semelhante à classe TouchEffect
do Android.
Importante
Muitas das exibições na UIKit
não têm toque habilitado por padrão. O toque pode ser habilitado adicionando view.UserInteractionEnabled = true;
para substituir OnAttached
na classe TouchEffect
no projeto do iOS. Isso deve ocorrer após a obtenção do UIView
, que corresponde ao elemento ao qual o efeito está anexado.
Colocando o efeito de toque para funcionar
O programa de exemplo contém cinco páginas que testam o efeito de rastreamento por toque para tarefas comuns.
A página Arrastando BoxView permite que você adicione elementos BoxView
a um AbsoluteLayout
e, em seguida, arraste-os na tela. O arquivo XAML cria uma instância de duas exibições Button
para adicionar elementos BoxView
ao AbsoluteLayout
e desmarcar o AbsoluteLayout
.
O método no arquivo code-behind que adiciona uma nova BoxView
ao AbsoluteLayout
também adiciona um objeto TouchEffect
à BoxView
e anexa um manipulador de eventos ao efeito:
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);
}
O manipulador de eventos TouchAction
processa todos os eventos de toque para todos os elementos BoxView
, mas precisa ter algum cuidado: ele não pode permitir dois dedos em uma única BoxView
porque o programa implementa apenas a operação de arrastar e os dois dedos interferirão um com o outro. Por esse motivo, a página define uma classe inserida para cada dedo acompanhado no momento:
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>();
O dragDictionary
contém uma entrada para cada BoxView
que está sendo arrastado no momento.
A ação de toque Pressed
adiciona um item a esse dicionário e a ação Released
remove-o. A lógica Pressed
precisa verificar se já há um item no dicionário para esse BoxView
. Nesse caso, a BoxView
já está sendo arrastada e o novo evento é um segundo dedo sobre essa mesma BoxView
. Para as ações Moved
e Released
, o manipulador de eventos precisa verificar se o dicionário tem uma entrada para essa BoxView
e se a propriedade Id
do toque para essa BoxView
arrastada corresponde à entrada do dicionário:
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;
}
}
A lógica Pressed
define a propriedade Capture
do objeto TouchEffect
como true
. Isso tem o efeito de fornecer todos os eventos seguintes desse dedo ao mesmo manipulador de eventos.
A lógica Moved
move a BoxView
alterando a propriedade anexada LayoutBounds
. A propriedade Location
dos argumentos do evento sempre é relativa à BoxView
que está sendo arrastada e, se a BoxView
estiver sendo arrastada a uma taxa constante, as propriedades Location
dos eventos consecutivos serão aproximadamente as mesmas. Por exemplo, se um dedo pressiona a BoxView
em seu centro, a ação Pressed
armazena uma propriedade PressPoint
de (50, 50), que permanece a mesma para eventos seguintes. Se a BoxView
é arrastada diagonalmente a uma taxa constante, as propriedades Location
seguintes durante a ação Moved
podem ser valores de (55, 55); nesse caso, a lógica Moved
adiciona 5 à posição horizontal e vertical da BoxView
. Isso move a BoxView
para que seu centro fique novamente diretamente sob o dedo.
Você pode mover vários elementos BoxView
simultaneamente usando dedos diferentes.
Criando subclasses da exibição
Muitas vezes, é mais fácil para um Xamarin.Forms elemento lidar com seus próprios eventos de toque. A página Arrastando a BoxView arrastável funciona da mesma maneira que a página Arrastando BoxView, mas os elementos que o usuário arrasta são instâncias de uma classe DraggableBoxView
que 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;
}
}
}
O construtor cria e anexa o TouchEffect
e define a propriedade Capture
quando é criada uma instância desse objeto pela primeira vez. Nenhum dicionário é necessário porque a própria classe armazena os valores isBeingDragged
, pressPoint
e touchId
associados a cada dedo. A manipulação de Moved
altera as propriedades TranslationX
e TranslationY
e, portanto, a lógica funcionará mesmo se o pai da DraggableBoxView
não for um AbsoluteLayout
.
Integração com o SkiaSharp
As duas próximas demonstrações exigem elementos gráficos e usam o SkiaSharp para essa finalidade. Você pode querer aprender sobre como usar o SkiaSharp antes de Xamarin.Forms estudar esses exemplos. Os dois primeiros artigos ("Noções básicas de desenho do SkiaSharp" e "Linhas e caminhos do SkiaSharp") abrangem tudo o que você precisará aqui.
A página Desenho de elipse permite que você desenhe uma elipse passando o dedo na tela. Dependendo de como você mover o dedo, você poderá desenhar a elipse do canto superior esquerdo para o canto inferior direito ou de qualquer outro canto para o canto oposto. A elipse é desenhada com uma cor e uma opacidade aleatórias.
Se, em seguida, você tocar uma das elipses, poderá arrastá-la para outra localização. Isso exige uma técnica conhecida como "teste de clique," que envolve a pesquisa do objeto gráfico em um ponto específico. As elipses SkiaSharp não Xamarin.Forms são elementos, portanto, não podem executar seu próprio TouchEffect
processamento. O TouchEffect
precisa ser aplicada a todo objeto da SKCanvasView
.
O arquivo EllipseDrawPage.xaml cria uma instância da SKCanvasView
em uma Grid
de célula única. O objeto TouchEffect
é anexado a esse 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>
No Android e na Plataforma Universal do Windows, o TouchEffect
pode ser anexado diretamente à SKCanvasView
, mas no iOS isso não funciona. Observe que a propriedade Capture
está definida como true
.
Cada elipse renderizada pelo SkiaSharp é representada por um objeto do 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;
}
}
As propriedades StartPoint
e EndPoint
são usadas quando o programa está processando a entrada por toque; a propriedade Rectangle
é usada para desenhar a elipse. A propriedade LastFingerLocation
entra em jogo quando a elipse está sendo arrastada e o método IsInEllipse
ajuda no teste de clique. O método retorna true
se o ponto está dentro da elipse.
O arquivo code-behind mantém três coleções:
Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();
O dicionário draggingFigure
contém um subconjunto da coleção completedFigures
. O manipulador de eventos PaintSurface
do SkiaSharp apenas renderiza os objetos nestas coleções completedFigures
e 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);
}
}
A parte mais complicada do processamento de toque é a manipulação de Pressed
. É nesse momento que o teste de clique é executado, mas se o código detectar uma elipse sob o dedo do usuário, essa elipse só poderá ser arrastada se não estiver sendo arrastada por outro dedo. Se não houver nenhuma elipse sob o dedo do usuário, o código iniciará o processo de desenho de uma nova elipse:
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;
Outro exemplo do SkiaSharp é a página Pintura a dedo. Você pode selecionar uma cor e uma largura do traço em duas exibições Picker
e, em seguida, desenhar com um ou mais dedos:
Este exemplo também exige uma classe separada para representar cada linha pintada na tela:
class FingerPaintPolyline
{
public FingerPaintPolyline()
{
Path = new SKPath();
}
public SKPath Path { set; get; }
public Color StrokeColor { set; get; }
public float StrokeWidth { set; get; }
}
Um objeto SKPath
é usado para renderizar cada linha. O arquivo FingerPaint.xaml.cs mantém duas coleções desses objetos, uma para essas polilinhas desenhadas no momento e outra para as polilinhas concluídas:
Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();
O processamento de Pressed
cria uma nova FingerPaintPolyline
, chama MoveTo
no objeto de caminho para armazenar o ponto inicial e adiciona esse objeto ao dicionário inProgressPolylines
. O processamento de Moved
chama LineTo
no objeto de caminho com a nova posição do dedo e o processamento de Released
transfere a polilinha concluída de inProgressPolylines
para completedPolylines
. Mais uma vez, o código de desenho real do SkiaSharp é relativamente simples:
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);
}
}
Acompanhando o toque exibição para exibição
Todos os exemplos anteriores definiram a propriedade Capture
do TouchEffect
como true
, quando o TouchEffect
foi criado ou quando o evento Pressed
ocorreu. Isso garante que o mesmo elemento receba todos os eventos associados ao dedo que pressionou a exibição primeiro. A amostra final não define Capture
como true
. Isso causa um comportamento diferente quando um dedo em contato com a tela se move de um elemento para outro. O elemento movido pelo dedo recebe um evento com uma propriedade Type
definida como TouchActionType.Exited
e o segundo elemento recebe um evento com uma configuração Type
igual a TouchActionType.Entered
.
Esse tipo de processamento de toque é muito útil para um teclado musical. Uma tecla deve conseguir detectar quando ela é pressionada, mas também quando um dedo deslizar de uma tecla para outra.
A página Teclado silencioso define classes WhiteKey
e BlackKey
pequenas que derivam de Key
, que, por sua vez, deriva de BoxView
.
A classe Key
está pronta para ser usada em um programa real de música. Ela define as propriedades públicas chamadas IsPressed
e KeyNumber
, que se destinam a ser definidas como o código de teclas estabelecido pelo padrão MIDI. A classe Key
também define um evento chamado StatusChanged
, que é invocado quando a propriedade IsPressed
é alterada.
Vários dedos são permitidos em cada tecla. Por esse motivo, a classe Key
mantém uma List
dos números de ID de toque dos dedos que estão tocando essa tecla:
List<long> ids = new List<long>();
O manipulador de eventos TouchAction
adiciona uma ID à lista ids
para um tipo de evento Pressed
e um tipo Entered
, mas somente quando a propriedade IsInContact
é true
para o evento Entered
. A ID é removida da List
para um evento Released
ou 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;
}
}
Os métodos AddToList
e RemoveFromList
verificam se a List
foi alterada entre vazia e não vazia e, nesse caso, invoca o evento StatusChanged
.
Os vários elementos WhiteKey
e BlackKey
são organizados no arquivo XAML da página, que tem uma melhor aparência quando o telefone é segurado em um modo paisagem:
Se você passar o dedo entre as teclas, pelas pequenas alterações nas cores, você verá que os eventos de toque são transferidos de uma tecla para outra.
Resumo
Este artigo demonstrou como invocar eventos em um efeito e como gravar e usar um efeito que implementa o processamento multitoque de nível baixo.