Manipulações de toque
Usar transformações de matriz para implementar arrastamento, pinçamento e rotação por toque
Em ambientes multitoque, como os de dispositivos móveis, os usuários costumam usar os dedos para manipular objetos na tela. Gestos comuns, como um arrastar de um dedo e uma pinça de dois dedos, podem mover e dimensionar objetos, ou até mesmo girá-los. Esses gestos geralmente são implementados usando matrizes de transformação, e este artigo mostra como fazer isso.
Todos os exemplos mostrados aqui usam o Xamarin.Forms efeito de rastreamento de toque apresentado no artigo Invocando eventos de efeitos.
Arrastamento e Tradução
Uma das aplicações mais importantes das transformações matriciais é o processamento tátil. Um único SKMatrix
valor pode consolidar uma série de operações de toque.
Para arrastar com um único dedo, o valor executa a SKMatrix
tradução. Isso é demonstrado na página Arrastar bitmap. O arquivo XAML instancia um SKCanvasView
em um Xamarin.FormsGrid
arquivo . Um TouchEffect
objeto foi adicionado à Effects
coleção desse Grid
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.BitmapDraggingPage"
Title="Bitmap Dragging">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
Em teoria, o TouchEffect
objeto poderia ser adicionado diretamente à Effects
coleção do SKCanvasView
, mas isso não funciona em todas as plataformas. Porque o SKCanvasView
é do mesmo tamanho que o Grid
nesta configuração, anexando-o Grid
às obras também.
O arquivo code-behind é carregado em um recurso de bitmap em seu construtor e o exibe no PaintSurface
manipulador:
public partial class BitmapDraggingPage : ContentPage
{
// Bitmap and matrix for display
SKBitmap bitmap;
SKMatrix matrix = SKMatrix.MakeIdentity();
···
public BitmapDraggingPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
canvas.SetMatrix(matrix);
canvas.DrawBitmap(bitmap, new SKPoint());
}
}
Sem nenhum código adicional, o SKMatrix
valor é sempre a matriz de identificação e não teria efeito sobre a exibição do bitmap. O objetivo do OnTouchEffectAction
manipulador definido no arquivo XAML é alterar o valor da matriz para refletir manipulações de toque.
O OnTouchEffectAction
manipulador começa convertendo o Xamarin.FormsPoint
valor em um valor SkiaSharp SKPoint
. Esta é uma questão simples de dimensionamento com base nas propriedades e Height
de Width
(que são unidades independentes de SKCanvasView
dispositivo) e na CanvasSize
propriedade, que está em unidades de pixels:
public partial class BitmapDraggingPage : ContentPage
{
···
// Touch information
long touchId = -1;
SKPoint previousPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point))
{
touchId = args.Id;
previousPoint = point;
}
break;
case TouchActionType.Moved:
if (touchId == args.Id)
{
// Adjust the matrix for the new position
matrix.TransX += point.X - previousPoint.X;
matrix.TransY += point.Y - previousPoint.Y;
previousPoint = point;
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = -1;
break;
}
}
···
}
Quando um dedo toca pela primeira vez na tela, um evento do tipo TouchActionType.Pressed
é disparado. A primeira tarefa é determinar se o dedo está tocando o bitmap. Tal tarefa é muitas vezes chamada de teste de acerto. Nesse caso, o teste de acerto pode ser realizado criando um SKRect
valor correspondente ao bitmap, aplicando a transformação de matriz a ele com MapRect
o e determinando se o ponto de toque está dentro do retângulo transformado.
Se esse for o caso, o touchId
campo será definido como ID de toque e a posição do dedo será salva.
Para o TouchActionType.Moved
evento, os fatores de translação do SKMatrix
valor são ajustados com base na posição atual do dedo e na nova posição do dedo. Essa nova posição é salva para a próxima vez e a SKCanvasView
é invalidada.
Ao experimentar este programa, observe que você só pode arrastar o bitmap quando o dedo toca em uma área onde o bitmap é exibido. Embora essa restrição não seja muito importante para este programa, torna-se crucial ao manipular vários bitmaps.
Pinçar e dimensionar
O que você quer que aconteça quando dois dedos tocam o bitmap? Se os dois dedos se movem em paralelo, então você provavelmente deseja que o bitmap se mova junto com os dedos. Se os dois dedos executarem uma operação de pinça ou estiramento, convém que o bitmap seja girado (a ser discutido na próxima seção) ou dimensionado. Ao dimensionar um bitmap, faz mais sentido que os dois dedos permaneçam nas mesmas posições em relação ao bitmap e que o bitmap seja dimensionado de acordo.
Lidar com dois dedos ao mesmo tempo parece complicado, mas tenha em mente que o TouchAction
manipulador só recebe informações sobre um dedo de cada vez. Se dois dedos estiverem manipulando o bitmap, para cada evento, um dedo mudou de posição, mas o outro não mudou. No código da página Bitmap Scaling abaixo, o dedo que não mudou de posição é chamado de ponto de pivô porque a transformação é relativa a esse ponto.
Uma diferença entre este programa e o programa anterior é que vários IDs de toque devem ser salvos. Um dicionário é usado para essa finalidade, onde o ID de toque é a chave do dicionário e o valor do dicionário é a posição atual desse dedo:
public partial class BitmapScalingPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point) && !touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Add(args.Id, point);
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger scale and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index of non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points involved in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Scaling factors are ratios of those
float scaleX = newVector.X / oldVector.X;
float scaleY = newVector.Y / oldVector.Y;
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
// If something bad hasn't happened, calculate a scale and translation matrix
SKMatrix scaleMatrix =
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
SKMatrix.PostConcat(ref matrix, scaleMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
···
}
O tratamento da ação é quase o mesmo que o programa anterior, exceto que o ID e o ponto de Pressed
toque são adicionados ao dicionário. As Released
ações e Cancelled
removem a entrada do dicionário.
O manejo para a Moved
ação, no entanto, é mais complexo. Se houver apenas um dedo envolvido, o processamento é muito parecido com o programa anterior. Para dois ou mais dedos, o programa também deve obter informações do dicionário envolvendo o dedo que não está se movendo. Ele faz isso copiando as chaves do dicionário em uma matriz e, em seguida, comparando a primeira chave com o ID do dedo que está sendo movido. Isso permite que o programa obtenha o ponto de pivô correspondente ao dedo que não está se movendo.
Em seguida, o programa calcula dois vetores da nova posição do dedo em relação ao ponto de pivô e a posição antiga do dedo em relação ao ponto de pivô. As proporções desses vetores são fatores de escala. Como a divisão por zero é uma possibilidade, eles devem ser verificados para valores infinitos ou valores NaN (não um número). Se tudo estiver bem, uma transformação de dimensionamento será concatenada com o SKMatrix
valor salvo como um campo.
Ao experimentar esta página, você notará que pode arrastar o bitmap com um ou dois dedos ou dimensioná-lo com dois dedos. A escala é anisotrópica, o que significa que a escala pode ser diferente nas direções horizontal e vertical. Isso distorce a proporção, mas também permite que você inverta o bitmap para criar uma imagem espelhada. Você também pode descobrir que pode reduzir o bitmap para uma dimensão zero e ele desaparece. No código de produção, você vai querer se proteger contra isso.
Rotação de dois dedos
A página Girar bitmap permite que você use dois dedos para rotação ou dimensionamento isotrópico. O bitmap sempre mantém sua proporção correta. Usar dois dedos para rotação e escala anisotrópica não funciona muito bem porque o movimento dos dedos é muito semelhante para ambas as tarefas.
A primeira grande diferença neste programa é a lógica de teste de acertos. Os programas anteriores usavam o Contains
método de para determinar se o ponto de toque está dentro do retângulo transformado SKRect
que corresponde ao bitmap. Mas, à medida que o usuário manipula o bitmap, o bitmap pode ser girado e SKRect
não pode representar corretamente um retângulo girado. Você pode temer que a lógica de teste de acerto precise implementar geometria analítica bastante complexa nesse caso.
No entanto, um atalho está disponível: determinar se um ponto está dentro dos limites de um retângulo transformado é o mesmo que determinar se um ponto transformado inverso está dentro dos limites do retângulo não transformado. Esse é um cálculo muito mais fácil, e a lógica pode continuar a usar o método conveniente Contains
:
public partial class BitmapRotationPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (!touchDictionary.ContainsKey(args.Id))
{
// Invert the matrix
if (matrix.TryInvert(out SKMatrix inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(point);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
if (rect.Contains(transformedPoint))
{
touchDictionary.Add(args.Id, point);
}
}
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger rotate, scale, and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Isotropic scaling!
float scale = Magnitude(newVector) / Magnitude(oldVector);
if (!float.IsNaN(scale) && !float.IsInfinity(scale))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));
SKMatrix.PostConcat(ref matrix, touchMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
···
}
A lógica para o Moved
evento começa como o programa anterior. Dois vetores nomeados oldVector
e newVector
são calculados com base no ponto anterior e atual do dedo em movimento e no ponto de pivô do dedo imóvel. Mas então os ângulos desses vetores são determinados, e a diferença é o ângulo de rotação.
O dimensionamento também pode estar envolvido, de modo que o vetor antigo é girado com base no ângulo de rotação. A magnitude relativa dos dois vetores é agora o fator de escala. Observe que o mesmo scale
valor é usado para dimensionamento horizontal e vertical para que o dimensionamento seja isotrópico. O matrix
campo é ajustado pela matriz de rotação e por uma matriz de escala.
Se seu aplicativo precisar implementar o processamento por toque para um único bitmap (ou outro objeto), você poderá adaptar o código desses três exemplos para seu próprio aplicativo. Mas se você precisar implementar o processamento por toque para vários bitmaps, provavelmente desejará encapsular essas operações de toque em outras classes.
Encapsulando as operações de toque
A página Manipulação de toque demonstra a manipulação de toque de um único bitmap, mas usando vários outros arquivos que encapsulam grande parte da lógica mostrada acima. O primeiro desses arquivos é a TouchManipulationMode
enumeração, que indica os diferentes tipos de manipulação de toque implementados pelo código que você verá:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
é um arrasto de um dedo que é implementado com a tradução. Todas as opções subsequentes também incluem movimento panorâmico, mas envolvem dois dedos: IsotropicScale
é uma operação de pinça que resulta no dimensionamento do objeto igualmente nas direções horizontal e vertical. AnisotropicScale
permite escalonamento desigual.
A ScaleRotate
opção é para escalonamento e rotação com dois dedos. A escala é isotrópica. Como mencionado anteriormente, a implementação da rotação de dois dedos com escala anisotrópica é problemática porque os movimentos dos dedos são essencialmente os mesmos.
A ScaleDualRotate
opção adiciona rotação de um dedo. Quando um único dedo arrasta o objeto, o objeto arrastado é primeiro girado em torno de seu centro para que o centro do objeto se alinhe com o vetor de arrastamento.
O arquivo TouchManipulationPage.xaml inclui um Picker
com os TouchManipulationMode
membros da enumeração:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
Title="Touch Manipulation">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker Title="Touch Mode"
Grid.Row="0"
SelectedIndexChanged="OnTouchModePickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:TouchManipulationMode}">
<x:Static Member="local:TouchManipulationMode.None" />
<x:Static Member="local:TouchManipulationMode.PanOnly" />
<x:Static Member="local:TouchManipulationMode.IsotropicScale" />
<x:Static Member="local:TouchManipulationMode.AnisotropicScale" />
<x:Static Member="local:TouchManipulationMode.ScaleRotate" />
<x:Static Member="local:TouchManipulationMode.ScaleDualRotate" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
4
</Picker.SelectedIndex>
</Picker>
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</Grid>
</ContentPage>
Na parte inferior está um SKCanvasView
e um TouchEffect
ligado à célula Grid
única que o encerra.
O arquivo code-behind TouchManipulationPage.xaml.cs tem um bitmap
campo, mas não é do tipo SKBitmap
. O tipo é TouchManipulationBitmap
(uma classe que você verá em breve):
public partial class TouchManipulationPage : ContentPage
{
TouchManipulationBitmap bitmap;
...
public TouchManipulationPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
this.bitmap = new TouchManipulationBitmap(bitmap);
this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
}
}
...
}
O construtor instancia um TouchManipulationBitmap
objeto, passando para o construtor um SKBitmap
obtido de um recurso incorporado. O construtor conclui definindo a Mode
TouchManager
propriedade da propriedade do TouchManipulationBitmap
objeto como um membro da TouchManipulationMode
enumeração.
O SelectedIndexChanged
manipulador para o Picker
também define esta Mode
propriedade:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
O TouchAction
manipulador do TouchEffect
instanciado no arquivo XAML chama dois métodos em TouchManipulationBitmap
named HitTest
e ProcessTouchEvent
:
public partial class TouchManipulationPage : ContentPage
{
...
List<long> touchIds = new List<long>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (bitmap.HitTest(point))
{
touchIds.Add(args.Id);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
break;
}
break;
case TouchActionType.Moved:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
touchIds.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
Se o HitTest
método retornar true
— o que significa que um dedo tocou na tela dentro da área ocupada pelo bitmap — o ID de toque será adicionado à TouchIds
coleção. Esse ID representa a sequência de eventos de toque para esse dedo até que o dedo seja levantado da tela. Se vários dedos tocarem no bitmap, a touchIds
coleção conterá uma ID de toque para cada dedo.
O TouchAction
manipulador também chama a ProcessTouchEvent
classe em TouchManipulationBitmap
. É aqui que ocorre parte (mas não todas) do processamento de toque real.
A TouchManipulationBitmap
classe é uma classe wrapper para SKBitmap
que contém código para renderizar o bitmap e processar eventos de toque. Ele funciona em conjunto com código mais generalizado em uma TouchManipulationManager
classe (que você verá em breve).
O TouchManipulationBitmap
construtor salva o SKBitmap
e instancia duas propriedades, a TouchManager
propriedade de type TouchManipulationManager
e a Matrix
propriedade de type SKMatrix
:
class TouchManipulationBitmap
{
SKBitmap bitmap;
...
public TouchManipulationBitmap(SKBitmap bitmap)
{
this.bitmap = bitmap;
Matrix = SKMatrix.MakeIdentity();
TouchManager = new TouchManipulationManager
{
Mode = TouchManipulationMode.ScaleRotate
};
}
public TouchManipulationManager TouchManager { set; get; }
public SKMatrix Matrix { set; get; }
...
}
Essa Matrix
propriedade é a transformação acumulada resultante de toda a atividade de toque. Como você verá, cada evento de toque é resolvido em uma matriz, que é então concatenada com o SKMatrix
valor armazenado pela Matrix
propriedade.
O TouchManipulationBitmap
objeto desenha a si mesmo em seu Paint
método. O argumento é um SKCanvas
objeto. Isso SKCanvas
já pode ter uma transformação aplicada a ele, portanto, o Paint
método concatena a Matrix
propriedade associada ao bitmap à transformação existente e restaura a tela quando ela terminar:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
O HitTest
método retorna true
se o usuário tocar na tela em um ponto dentro dos limites do bitmap. Isso usa a lógica mostrada anteriormente na página Rotação de bitmap :
class TouchManipulationBitmap
{
...
public bool HitTest(SKPoint location)
{
// Invert the matrix
SKMatrix inverseMatrix;
if (Matrix.TryInvert(out inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(location);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
return rect.Contains(transformedPoint);
}
return false;
}
...
}
O segundo método público em TouchManipulationBitmap
é ProcessTouchEvent
. Quando esse método é chamado, já foi estabelecido que o evento touch pertence a esse bitmap específico. O método mantém um dicionário de TouchManipulationInfo
objetos, que é simplesmente o ponto anterior e o novo ponto de cada dedo:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Aqui está o dicionário e o ProcessTouchEvent
método em si:
class TouchManipulationBitmap
{
...
Dictionary<long, TouchManipulationInfo> touchDictionary =
new Dictionary<long, TouchManipulationInfo>();
...
public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
{
switch (type)
{
case TouchActionType.Pressed:
touchDictionary.Add(id, new TouchManipulationInfo
{
PreviousPoint = location,
NewPoint = location
});
break;
case TouchActionType.Moved:
TouchManipulationInfo info = touchDictionary[id];
info.NewPoint = location;
Manipulate();
info.PreviousPoint = info.NewPoint;
break;
case TouchActionType.Released:
touchDictionary[id].NewPoint = location;
Manipulate();
touchDictionary.Remove(id);
break;
case TouchActionType.Cancelled:
touchDictionary.Remove(id);
break;
}
}
...
}
Moved
Nos eventos e Released
, o método chama Manipulate
. Nessas horas, o touchDictionary
contém um ou mais TouchManipulationInfo
objetos. Se o touchDictionary
contém um item, é provável que os PreviousPoint
valores e NewPoint
sejam desiguais e representem o movimento de um dedo. Se vários dedos estiverem tocando no bitmap, o dicionário conterá mais de um item, mas apenas um desses itens terá valores e PreviousPoint
NewPoint
diferentes. Todos os demais têm valores iguais PreviousPoint
e NewPoint
iguais.
Isso é importante: o Manipulate
método pode assumir que está processando o movimento de apenas um dedo. No momento desta chamada, nenhum dos outros dedos está se movendo, e se eles realmente estão se movendo (como é provável), esses movimentos serão processados em chamadas futuras para Manipulate
.
O Manipulate
método primeiro copia o dicionário para uma matriz por conveniência. Ele ignora qualquer coisa além das duas primeiras entradas. Se mais de dois dedos estiverem tentando manipular o bitmap, os outros serão ignorados. Manipulate
é o membro final de TouchManipulationBitmap
:
class TouchManipulationBitmap
{
...
void Manipulate()
{
TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
touchDictionary.Values.CopyTo(infos, 0);
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
if (infos.Length == 1)
{
SKPoint prevPoint = infos[0].PreviousPoint;
SKPoint newPoint = infos[0].NewPoint;
SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);
touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
}
else if (infos.Length >= 2)
{
int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
SKPoint pivotPoint = infos[pivotIndex].NewPoint;
SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;
touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
}
SKMatrix matrix = Matrix;
SKMatrix.PostConcat(ref matrix, touchMatrix);
Matrix = matrix;
}
}
Se um dedo estiver manipulando o bitmap, Manipulate
chame o OneFingerManipulate
método do TouchManipulationManager
objeto. Para dois dedos, chama TwoFingerManipulate
. Os argumentos para esses métodos são os mesmos: os prevPoint
argumentos e newPoint
representam o dedo que está se movendo. Mas o pivotPoint
argumento é diferente para as duas chamadas:
Para manipulação com um dedo, o pivotPoint
é o centro do bitmap. Isso é para permitir a rotação de um dedo. Para a manipulação com dois dedos, o evento indica o movimento de apenas um dedo, de modo que o pivotPoint
dedo não está se movendo.
Em ambos os casos, TouchManipulationManager
retorna um SKMatrix
valor, que o método concatena com a propriedade atual Matrix
que TouchManipulationPage
usa para renderizar o bitmap.
TouchManipulationManager
é generalizado e não usa outros arquivos, exceto TouchManipulationMode
. Você pode usar essa classe sem alteração em seus próprios aplicativos. Ela define uma única propriedade do tipo TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
No entanto, você provavelmente vai querer evitar a AnisotropicScale
opção. É muito fácil com essa opção manipular o bitmap para que um dos fatores de dimensionamento se torne zero. Isso faz com que o bitmap desapareça de vista, para nunca mais voltar. Se você realmente precisa de escalonamento anisotrópico, você vai querer melhorar a lógica para evitar resultados indesejáveis.
TouchManipulationManager
faz uso de vetores, mas como não SKVector
há estrutura no SkiaSharp, SKPoint
é usado em vez disso. SKPoint
suporta o operador de subtração, e o resultado pode ser tratado como um vetor. A única lógica específica do vetor que precisava ser adicionada é um Magnitude
cálculo:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Sempre que a rotação foi selecionada, os métodos de manipulação com um e dois dedos manipulam a rotação primeiro. Se qualquer rotação for detectada, o componente de rotação será efetivamente removido. O que resta é interpretado como movimento panorâmico e dimensionamento.
Aqui está o OneFingerManipulate
método. Se a rotação de um dedo não foi habilitada, então a lógica é simples - ela simplesmente usa o ponto anterior e o novo ponto para construir um vetor chamado delta
que corresponde precisamente à tradução. Com a rotação de um dedo habilitada, o método usa ângulos do ponto dinâmico (o centro do bitmap) para o ponto anterior e novo ponto para construir uma matriz de rotação:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
if (Mode == TouchManipulationMode.None)
{
return SKMatrix.MakeIdentity();
}
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint delta = newPoint - prevPoint;
if (Mode == TouchManipulationMode.ScaleDualRotate) // One-finger rotation
{
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Avoid rotation if fingers are too close to center
if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
{
float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - prevAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Recalculate delta
delta = newVector - oldVector;
}
}
// Multiply the rotation matrix by a translation matrix
SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));
return touchMatrix;
}
...
}
No método, o TwoFingerManipulate
ponto de pivô é a posição do dedo que não está se movendo nesse evento de toque específico. A rotação é muito semelhante à rotação de um dedo, e então o vetor nomeado oldVector
(com base no ponto anterior) é ajustado para a rotação. O movimento restante é interpretado como escala:
class TouchManipulationManager
{
...
public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
if (Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
}
float scaleX = 1;
float scaleY = 1;
if (Mode == TouchManipulationMode.AnisotropicScale)
{
scaleX = newVector.X / oldVector.X;
scaleY = newVector.Y / oldVector.Y;
}
else if (Mode == TouchManipulationMode.IsotropicScale ||
Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
}
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
}
return touchMatrix;
}
...
}
Você notará que não há tradução explícita nesse método. No entanto, os MakeRotation
métodos e MakeScale
são baseados no ponto de pivô, e isso inclui a tradução implícita. Se você estiver usando dois dedos no bitmap e arrastando-os na mesma direção, TouchManipulation
obterá uma série de eventos de toque alternando entre os dois dedos. À medida que cada dedo se move em relação ao outro, a escala ou rotação resulta, mas é negada pelo movimento do outro dedo, e o resultado é a translação.
A única parte restante da página Manipulação de toque é o PaintSurface
manipulador no TouchManipulationPage
arquivo code-behind. Isso chama o Paint
TouchManipulationBitmap
método do , que aplica a matriz que representa a atividade de toque acumulada:
public partial class TouchManipulationPage : ContentPage
{
...
MatrixDisplay matrixDisplay = new MatrixDisplay();
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
bitmap.Paint(canvas);
// Display the matrix in the lower-right corner
SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);
matrixDisplay.Paint(canvas, bitmap.Matrix,
new SKPoint(info.Width - matrixSize.Width,
info.Height - matrixSize.Height));
}
}
O PaintSurface
manipulador conclui exibindo um MatrixDisplay
objeto mostrando a matriz de toque acumulada:
Manipulando vários bitmaps
Uma das vantagens de isolar o código de processamento por toque em classes como TouchManipulationBitmap
e TouchManipulationManager
é a capacidade de reutilizar essas classes em um programa que permite ao usuário manipular vários bitmaps.
A página Exibição de dispersão de bitmap demonstra como isso é feito. Em vez de definir um campo de tipo TouchManipulationBitmap
, a BitmapScatterPage
classe define um List
dos objetos bitmap:
public partial class BitmapScatterViewPage : ContentPage
{
List<TouchManipulationBitmap> bitmapCollection =
new List<TouchManipulationBitmap>();
...
public BitmapScatterViewPage()
{
InitializeComponent();
// Load in all the available bitmaps
Assembly assembly = GetType().GetTypeInfo().Assembly;
string[] resourceIDs = assembly.GetManifestResourceNames();
SKPoint position = new SKPoint();
foreach (string resourceID in resourceIDs)
{
if (resourceID.EndsWith(".png") ||
resourceID.EndsWith(".jpg"))
{
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
{
Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
});
position.X += 100;
position.Y += 100;
}
}
}
}
...
}
O construtor carrega todos os bitmaps disponíveis como recursos incorporados e os adiciona ao bitmapCollection
. Observe que a Matrix
propriedade é inicializada em cada TouchManipulationBitmap
objeto, portanto, os cantos superiores esquerdos de cada bitmap são deslocados em 100 pixels.
A BitmapScatterView
página também precisa manipular eventos de toque para vários bitmaps. Em vez de definir um List
dos IDs de toque de objetos manipulados TouchManipulationBitmap
atualmente, este programa requer um dicionário:
public partial class BitmapScatterViewPage : ContentPage
{
...
Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
new Dictionary<long, TouchManipulationBitmap>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
for (int i = bitmapCollection.Count - 1; i >= 0; i--)
{
TouchManipulationBitmap bitmap = bitmapCollection[i];
if (bitmap.HitTest(point))
{
// Move bitmap to end of collection
bitmapCollection.Remove(bitmap);
bitmapCollection.Add(bitmap);
// Do the touch processing
bitmapDictionary.Add(args.Id, bitmap);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
break;
}
}
break;
case TouchActionType.Moved:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
bitmapDictionary.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
Observe como a Pressed
lógica faz um loop inverso bitmapCollection
. Os bitmaps geralmente se sobrepõem. Os bitmaps posteriores na coleção ficam visualmente sobre os bitmaps anteriores na coleção. Se houver vários bitmaps sob o dedo que pressiona na tela, o mais alto deve ser aquele que é manipulado por esse dedo.
Observe também que a Pressed
lógica move esse bitmap para o final da coleção para que ele se mova visualmente para o topo da pilha de outros bitmaps.
Moved
Nos eventos e Released
, o TouchAction
manipulador chama o ProcessingTouchEvent
método como TouchManipulationBitmap
o programa anterior.
Finalmente, o PaintSurface
manipulador chama o Paint
método de cada TouchManipulationBitmap
objeto:
public partial class BitmapScatterViewPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (TouchManipulationBitmap bitmap in bitmapCollection)
{
bitmap.Paint(canvas);
}
}
}
O código percorre a coleção e exibe a pilha de bitmaps do início à final:
Dimensionamento com um único dedo
Uma operação de dimensionamento geralmente requer um gesto de pinça usando dois dedos. No entanto, é possível implementar o dimensionamento com um único dedo fazendo com que o dedo mova os cantos de um bitmap.
Isso é demonstrado na página Escala de canto de dedo único. Como esse exemplo usa um tipo de dimensionamento um pouco diferente daquele implementado na TouchManipulationManager
classe, ele não usa essa classe ou a TouchManipulationBitmap
classe. Em vez disso, toda a lógica de toque está no arquivo code-behind. Essa é uma lógica um pouco mais simples do que o normal, porque rastreia apenas um dedo de cada vez e simplesmente ignora quaisquer dedos secundários que possam estar tocando na tela.
A página SingleFingerCornerScale.xaml instancia a SKCanvasView
classe e cria um TouchEffect
objeto para controlar eventos de toque:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
Title="Single Finger Corner Scale">
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
O arquivo SingleFingerCornerScalePage.xaml.cs carrega um recurso de bitmap do diretório Media e o exibe usando um SKMatrix
objeto definido como um campo:
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
···
public SingleFingerCornerScalePage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.SetMatrix(currentMatrix);
canvas.DrawBitmap(bitmap, 0, 0);
}
···
}
Este SKMatrix
objeto é modificado pela lógica de toque mostrada abaixo.
O restante do arquivo code-behind é o TouchEffect
manipulador de eventos. Ele começa convertendo o local atual do dedo em um SKPoint
valor. Para o Pressed
tipo de ação, o manipulador verifica se nenhum outro dedo está tocando na tela e se o dedo está dentro dos limites do bitmap.
A parte crucial do código é uma if
instrução envolvendo duas chamadas para o Math.Pow
método. Essa matemática verifica se o local do dedo está fora de uma elipse que preenche o bitmap. Se sim, então essa é uma operação de dimensionamento. O dedo está perto de um dos cantos do bitmap e um ponto de pivô é determinado que é o canto oposto. Se o dedo estiver dentro dessa elipse, é uma operação de movimento panorâmico regular:
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
// Information for translating and scaling
long? touchId = null;
SKPoint pressedLocation;
SKMatrix pressedMatrix;
// Information for scaling
bool isScaling;
SKPoint pivotPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Track only one finger
if (touchId.HasValue)
return;
// Check if the finger is within the boundaries of the bitmap
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = currentMatrix.MapRect(rect);
if (!rect.Contains(point))
return;
// First assume there will be no scaling
isScaling = false;
// If touch is outside interior ellipse, make this a scaling operation
if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
{
isScaling = true;
float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
pivotPoint = new SKPoint(xPivot, yPivot);
}
// Common for either pan or scale
touchId = args.Id;
pressedLocation = point;
pressedMatrix = currentMatrix;
break;
case TouchActionType.Moved:
if (!touchId.HasValue || args.Id != touchId.Value)
return;
SKMatrix matrix = SKMatrix.MakeIdentity();
// Translating
if (!isScaling)
{
SKPoint delta = point - pressedLocation;
matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
}
// Scaling
else
{
float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
}
// Concatenate the matrices
SKMatrix.PreConcat(ref matrix, pressedMatrix);
currentMatrix = matrix;
canvasView.InvalidateSurface();
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = null;
break;
}
}
}
O Moved
tipo de ação calcula uma matriz correspondente à atividade de toque desde o momento em que o dedo pressionou a tela até esse momento. Ele concatena essa matriz com a matriz em vigor no momento em que o dedo pressionou o bitmap pela primeira vez. A operação de dimensionamento é sempre relativa ao canto oposto àquele que o dedo tocou.
Para bitmaps pequenos ou oblongos, uma elipse interna pode ocupar a maior parte do bitmap e deixar pequenas áreas nos cantos para dimensionar o bitmap. Você pode preferir uma abordagem um pouco diferente, caso em que você pode substituir todo if
o bloco que define true
isScaling
com este código:
float halfHeight = rect.Height / 2;
float halfWidth = rect.Width / 2;
// Top half of bitmap
if (point.Y < rect.MidY)
{
float yRelative = (point.Y - rect.Top) / halfHeight;
// Upper-left corner
if (point.X < rect.MidX - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Bottom);
}
// Upper-right corner
else if (point.X > rect.MidX + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Bottom);
}
}
// Bottom half of bitmap
else
{
float yRelative = (point.Y - rect.MidY) / halfHeight;
// Lower-left corner
if (point.X < rect.Left + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Top);
}
// Lower-right corner
else if (point.X > rect.Right - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Top);
}
}
Esse código efetivamente divide a área do bitmap em uma forma de diamante interior e quatro triângulos nos cantos. Isso permite que áreas muito maiores nos cantos agarrem e dimensionem o bitmap.