Manipolazioni tramite tocco
Usare le trasformazioni della matrice per implementare il trascinamento del tocco, il avvicinamento delle dita e la rotazione
Negli ambienti multitocco, ad esempio quelli nei dispositivi mobili, gli utenti spesso usano le dita per modificare gli oggetti sullo schermo. Movimenti comuni, ad esempio un trascinamento con un dito e un avvicinamento con due dita, possono spostare e ridimensionare oggetti o anche ruotarli. Questi movimenti vengono in genere implementati usando matrici di trasformazione e questo articolo illustra come eseguire questa operazione.
Tutti gli esempi illustrati qui usano l'effetto Xamarin.Forms di rilevamento del tocco presentato nell'articolo Richiamo di eventi da Effetti.
Trascinamento e traduzione
Una delle applicazioni più importanti delle trasformazioni della matrice è l'elaborazione del tocco. Un singolo SKMatrix
valore può consolidare una serie di operazioni di tocco.
Per il trascinamento con un solo dito, il valore esegue la SKMatrix
conversione. Questa operazione è illustrata nella pagina Di trascinamento bitmap. Il file XAML crea un'istanza SKCanvasView
di in un oggetto Xamarin.FormsGrid
. Un TouchEffect
oggetto è stato aggiunto all'insieme Effects
di :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>
In teoria, l'oggetto TouchEffect
può essere aggiunto direttamente all'insieme Effects
di SKCanvasView
, ma non funziona su tutte le piattaforme. Poiché è SKCanvasView
la stessa dimensione di Grid
in questa configurazione, il collegamento all'oggetto Grid
funziona altrettanto bene.
Il file code-behind viene caricato in una risorsa bitmap nel relativo costruttore e lo visualizza nel PaintSurface
gestore:
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());
}
}
Senza un altro codice, il SKMatrix
valore è sempre la matrice di identificazione e non avrà alcun effetto sulla visualizzazione della bitmap. L'obiettivo del OnTouchEffectAction
gestore impostato nel file XAML consiste nel modificare il valore della matrice in modo da riflettere le manipolazioni del tocco.
Il OnTouchEffectAction
gestore inizia convertendo il Xamarin.FormsPoint
valore in un valore SkiaSharp SKPoint
. Si tratta di una semplice questione di ridimensionamento in base alle Width
proprietà e Height
di SKCanvasView
(che sono unità indipendenti dal dispositivo) e alla CanvasSize
proprietà, espressa in unità di pixel:
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 un dito tocca per la prima volta lo schermo, viene generato un evento di tipo TouchActionType.Pressed
. La prima attività consiste nel determinare se il dito tocca la bitmap. Un'attività di questo tipo viene spesso chiamata hit testing. In questo caso, è possibile eseguire l'hit testing creando un SKRect
valore corrispondente alla bitmap, applicando la trasformazione matrice a essa con MapRect
e quindi determinando se il punto di tocco si trova all'interno del rettangolo trasformato.
In questo caso, il touchId
campo viene impostato sull'ID tocco e la posizione del dito viene salvata.
Per l'evento TouchActionType.Moved
, i fattori di traslazione del SKMatrix
valore vengono regolati in base alla posizione corrente del dito e alla nuova posizione del dito. La nuova posizione viene salvata per la volta successiva e l'oggetto SKCanvasView
viene invalidato.
Durante l'esperimento con questo programma, tieni presente che puoi trascinare la bitmap solo quando il dito tocca un'area in cui viene visualizzata la bitmap. Anche se questa restrizione non è molto importante per questo programma, diventa fondamentale quando si modificano più bitmap.
Avvicinamento delle dita e ridimensionamento
Cosa vuoi accadere quando due dita toccano la bitmap? Se le due dita si spostano in parallelo, è probabile che la bitmap si muova insieme alle dita. Se le due dita eseguono un'operazione di avvicinamento o di estensione, è possibile che la bitmap venga ruotata (da discutere nella sezione successiva) o ridimensionata. Quando si ridimensiona una bitmap, è più opportuno che le due dita rimangano nelle stesse posizioni rispetto alla bitmap e che la bitmap venga ridimensionata di conseguenza.
La gestione di due dita contemporaneamente sembra complicata, ma tenere presente che il TouchAction
gestore riceve solo informazioni su un dito alla volta. Se due dita stanno manipolando la bitmap, per ogni evento, un dito ha cambiato posizione, ma l'altro non è cambiato. Nel codice della pagina Ridimensionamento bitmap riportato di seguito, il dito che non ha modificato la posizione viene chiamato punto pivot perché la trasformazione è relativa a quel punto.
Una differenza tra questo programma e il programma precedente è che è necessario salvare più ID tocco. A questo scopo viene usato un dizionario, in cui l'ID tocco è la chiave del dizionario e il valore del dizionario è la posizione corrente del dito:
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;
}
}
···
}
La gestione dell'azione Pressed
è quasi identica al programma precedente, ad eccezione del fatto che l'ID e il punto di tocco vengono aggiunti al dizionario. Le Released
azioni e Cancelled
rimuovono la voce del dizionario.
La gestione dell'azione Moved
è tuttavia più complessa. Se c'è un solo dito coinvolto, l'elaborazione è molto uguale al programma precedente. Per due o più dita, il programma deve anche ottenere informazioni dal dizionario che coinvolge il dito che non si muove. A tale scopo, copiare le chiavi del dizionario in una matrice e quindi confrontare la prima chiave con l'ID del dito spostato. Ciò consente al programma di ottenere il punto pivot corrispondente al dito che non si muove.
Successivamente, il programma calcola due vettori della nuova posizione del dito rispetto al punto pivot e la posizione precedente del dito rispetto al punto pivot. I rapporti di questi vettori sono fattori di ridimensionamento. Poiché la divisione per zero è una possibilità, questi devono essere controllati per i valori infiniti o NaN (non un numero). Se tutto è corretto, una trasformazione di ridimensionamento viene concatenata con il SKMatrix
valore salvato come campo.
Durante l'esperimento con questa pagina, si noterà che è possibile trascinare la bitmap con una o due dita o scalarla con due dita. Il ridimensionamento è anisotropico, il che significa che il ridimensionamento può essere diverso nelle direzioni orizzontali e verticali. In questo modo le proporzioni vengono distorte, ma consentono anche di capovolgere la bitmap per creare un'immagine mirror. Si potrebbe anche scoprire che è possibile compattare la bitmap a una dimensione zero e scompare. Nel codice di produzione è consigliabile proteggersi da questo problema.
Rotazione a due dita
La pagina Bitmap Rotate consente di usare due dita per la rotazione o la scala isotropica. La bitmap mantiene sempre le proporzioni corrette. L'uso di due dita per la rotazione e la scala anisotropica non funziona molto bene perché il movimento delle dita è molto simile per entrambe le attività.
La prima grande differenza in questo programma è la logica di hit testing. I programmi precedenti usavano il Contains
metodo di SKRect
per determinare se il punto di tocco si trova all'interno del rettangolo trasformato che corrisponde alla bitmap. Tuttavia, quando l'utente modifica la bitmap, la bitmap potrebbe essere ruotata e SKRect
non può rappresentare correttamente un rettangolo ruotato. È possibile temere che la logica di hit testing debba implementare una geometria analitica piuttosto complessa in questo caso.
Tuttavia, è disponibile un collegamento: determinare se un punto si trova all'interno dei limiti di un rettangolo trasformato è uguale a determinare se un punto trasformato inversa si trova all'interno dei limiti del rettangolo non trasformato. Si tratta di un calcolo molto più semplice e la logica può continuare a usare il metodo pratico 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));
}
···
}
La logica per l'evento Moved
inizia come il programma precedente. Due vettori denominati oldVector
e newVector
vengono calcolati in base al punto precedente e al punto corrente del dito mobile e al punto pivot del dito senza movimento. Tuttavia, gli angoli di questi vettori vengono determinati e la differenza è l'angolo di rotazione.
Il ridimensionamento potrebbe anche essere coinvolto, quindi il vettore precedente viene ruotato in base all'angolo di rotazione. La grandezza relativa dei due vettori è ora il fattore di ridimensionamento. Si noti che lo stesso scale
valore viene usato per la scalabilità orizzontale e verticale in modo che il ridimensionamento sia isotropico. Il matrix
campo viene regolato sia dalla matrice di rotazione che da una matrice di scala.
Se l'applicazione deve implementare l'elaborazione del tocco per una singola bitmap (o un altro oggetto), è possibile adattare il codice di questi tre esempi per la propria applicazione. Tuttavia, se è necessario implementare l'elaborazione del tocco per più bitmap, è probabile che si voglia incapsulare queste operazioni di tocco in altre classi.
Incapsulamento delle operazioni di tocco
La pagina Manipolazione tocco illustra la manipolazione del tocco di una singola bitmap, ma l'uso di diversi altri file che incapsulano gran parte della logica illustrata in precedenza. Il primo di questi file è l'enumerazione TouchManipulationMode
, che indica i diversi tipi di manipolazione del tocco implementati dal codice visualizzato:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
è un trascinamento con un dito implementato con la traduzione. Tutte le opzioni successive includono anche la panoramica, ma comportano due dita: IsotropicScale
è un'operazione di avvicinamento delle dita che comporta il ridimensionamento dell'oggetto in modo uniforme nelle direzioni orizzontali e verticali. AnisotropicScale
consente un ridimensionamento diverso.
L'opzione ScaleRotate
è per il ridimensionamento e la rotazione a due dita. Il ridimensionamento è isotropico. Come accennato in precedenza, l'implementazione della rotazione a due dita con scala anisotropica è problematica perché i movimenti delle dita sono essenzialmente gli stessi.
L'opzione ScaleDualRotate
aggiunge una rotazione di un dito. Quando un singolo dito trascina l'oggetto, l'oggetto trascinato viene prima ruotato intorno al suo centro in modo che il centro dell'oggetto si allinea con il vettore di trascinamento.
Il file TouchManipulationPage.xaml include un Picker
oggetto con i membri dell'enumerazione TouchManipulationMode
:
<?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>
Verso il basso è un oggetto SKCanvasView
e un TouchEffect
oggetto collegato alla cella Grid
singola che lo racchiude.
Il file code-behind TouchManipulationPage.xaml.cs ha un bitmap
campo, ma non è di tipo SKBitmap
. Il tipo è TouchManipulationBitmap
(una classe che verrà visualizzata a 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;
}
}
...
}
Il costruttore crea un'istanza di un TouchManipulationBitmap
oggetto, passando al costruttore un SKBitmap
oggetto ottenuto da una risorsa incorporata. Il costruttore termina impostando la Mode
proprietà della TouchManager
proprietà dell'oggetto TouchManipulationBitmap
su un membro dell'enumerazione TouchManipulationMode
.
Il SelectedIndexChanged
gestore per l'oggetto Picker
imposta anche questa Mode
proprietà:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
Il TouchAction
gestore dell'istanza TouchEffect
creata nel file XAML chiama due metodi in TouchManipulationBitmap
denominati 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 il HitTest
metodo restituisce true
, vale a dire che un dito ha toccato lo schermo all'interno dell'area occupata dalla bitmap, l'ID tocco viene aggiunto alla TouchIds
raccolta. Questo ID rappresenta la sequenza di eventi di tocco per il dito fino a quando il dito non si solleva dallo schermo. Se più dita toccano la bitmap, la touchIds
raccolta contiene un ID tocco per ogni dito.
Il TouchAction
gestore chiama anche la ProcessTouchEvent
classe in TouchManipulationBitmap
. Questo è il luogo in cui si verificano alcune (ma non tutte) dell'elaborazione del tocco reale.
La TouchManipulationBitmap
classe è una classe wrapper per SKBitmap
che contiene codice per il rendering degli eventi di tocco bitmap ed elaborazione. Funziona in combinazione con codice più generalizzato in una TouchManipulationManager
classe (che verrà visualizzata a breve).
Il TouchManipulationBitmap
costruttore salva e crea un'istanza SKBitmap
di due proprietà, la TouchManager
proprietà di tipo TouchManipulationManager
e la Matrix
proprietà di tipo 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; }
...
}
Questa Matrix
proprietà è la trasformazione accumulata risultante da tutte le attività di tocco. Come si vedrà, ogni evento di tocco viene risolto in una matrice, che viene quindi concatenata con il SKMatrix
valore archiviato dalla Matrix
proprietà .
L'oggetto TouchManipulationBitmap
disegna se stesso nel relativo Paint
metodo. L'argomento è un SKCanvas
oggetto . Potrebbe SKCanvas
essere già stata applicata una trasformazione, quindi il Paint
metodo concatena la Matrix
proprietà associata alla bitmap alla trasformazione esistente e ripristina l'area di disegno al termine dell'operazione:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
Il HitTest
metodo restituisce true
se l'utente tocca lo schermo in un punto entro i limiti della bitmap. In questo modo viene usata la logica illustrata in precedenza nella pagina Rotazione 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;
}
...
}
Il secondo metodo pubblico in TouchManipulationBitmap
è ProcessTouchEvent
. Quando questo metodo viene chiamato, è già stato stabilito che l'evento touch appartiene a questa bitmap specifica. Il metodo gestisce un dizionario di TouchManipulationInfo
oggetti, che è semplicemente il punto precedente e il nuovo punto di ogni dito:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Ecco il dizionario e il ProcessTouchEvent
metodo stesso:
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
Negli eventi e Released
il metodo chiama Manipulate
. In questi casi, contiene touchDictionary
uno o più TouchManipulationInfo
oggetti. Se contiene touchDictionary
un elemento, è probabile che i PreviousPoint
valori e NewPoint
siano diversi e rappresentino il movimento di un dito. Se più dita toccano la bitmap, il dizionario contiene più di un elemento, ma solo uno di questi elementi ha valori e NewPoint
diversiPreviousPoint
. Tutti gli altri hanno valori uguali PreviousPoint
e NewPoint
.
Questo è importante: il Manipulate
metodo può presupporre che stia elaborando il movimento di un solo dito. Al momento di questa chiamata nessuna delle altre dita è in movimento e se sono effettivamente in movimento (come è probabile), tali movimenti verranno elaborati nelle chiamate future a Manipulate
.
Il Manipulate
metodo copia innanzitutto il dizionario in una matrice per praticità. Ignora qualsiasi elemento diverso dalle prime due voci. Se più di due dita tentano di modificare la bitmap, gli altri vengono ignorati. Manipulate
è il membro finale di 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 un dito sta manipolando la bitmap, Manipulate
chiama il OneFingerManipulate
metodo dell'oggetto TouchManipulationManager
. Per due dita, chiama TwoFingerManipulate
. Gli argomenti di questi metodi sono gli stessi: gli prevPoint
argomenti e newPoint
rappresentano il dito che si sta spostando. Ma l'argomento pivotPoint
è diverso per le due chiamate:
Per la manipolazione di un dito, è pivotPoint
il centro della bitmap. Ciò consente la rotazione di un dito. Per la manipolazione a due dita, l'evento indica il movimento di un solo dito, in modo che sia pivotPoint
il dito che non si muove.
In entrambi i casi restituisce TouchManipulationManager
un SKMatrix
valore, che il metodo concatena con la proprietà corrente Matrix
che TouchManipulationPage
usa per eseguire il rendering della bitmap.
TouchManipulationManager
è generalizzato e non usa altri file ad eccezione TouchManipulationMode
di . Potrebbe essere possibile usare questa classe senza modifiche nelle proprie applicazioni. Definisce una singola proprietà di tipo TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Tuttavia, è probabile che si voglia evitare l'opzione AnisotropicScale
. È molto facile con questa opzione modificare la bitmap in modo che uno dei fattori di ridimensionamento diventi zero. In questo modo la bitmap scompare dalla vista, non torna mai. Se è veramente necessario un ridimensionamento anisotropico, è necessario migliorare la logica per evitare risultati indesiderati.
TouchManipulationManager
utilizza vettori, ma poiché non esiste alcuna SKVector
struttura in SkiaSharp, SKPoint
viene invece usato. SKPoint
supporta l'operatore di sottrazione e il risultato può essere considerato come vettore. L'unica logica specifica del vettore da aggiungere è un Magnitude
calcolo:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Ogni volta che è stata selezionata la rotazione, entrambi i metodi di manipolazione con un dito e due dita gestiscono prima la rotazione. Se viene rilevata una rotazione, il componente di rotazione viene effettivamente rimosso. Ciò che rimane viene interpretato come panoramica e ridimensionamento.
Ecco il OneFingerManipulate
metodo . Se la rotazione di un dito non è stata abilitata, la logica è semplice, ma usa semplicemente il punto precedente e il nuovo punto per costruire un vettore denominato delta
che corrisponde esattamente alla traduzione. Con la rotazione con un dito abilitato, il metodo usa angoli dal punto pivot (il centro della bitmap) al punto precedente e al nuovo punto per costruire una matrice di rotazione:
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;
}
...
}
TwoFingerManipulate
Nel metodo il punto pivot è la posizione del dito che non si sposta in questo particolare evento di tocco. La rotazione è molto simile alla rotazione di un dito e quindi il vettore denominato oldVector
(in base al punto precedente) viene regolato per la rotazione. Il movimento rimanente viene interpretato come ridimensionamento:
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;
}
...
}
Si noterà che in questo metodo non è presente alcuna traduzione esplicita. Tuttavia, entrambi i MakeRotation
metodi e MakeScale
sono basati sul punto pivot e che includono la traduzione implicita. Se si usano due dita sulla bitmap e trascinandoli nella stessa direzione, TouchManipulation
si otterrà una serie di eventi di tocco alternati tra le due dita. Man mano che ogni dito si sposta rispetto all'altro, il ridimensionamento o i risultati della rotazione, ma viene negato dal movimento dell'altro dito e il risultato è la traslazione.
L'unica parte rimanente della pagina Manipolazione tocco è il PaintSurface
gestore nel TouchManipulationPage
file code-behind. Viene chiamato il Paint
metodo di TouchManipulationBitmap
, che applica la matrice che rappresenta l'attività di tocco accumulata:
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));
}
}
Il PaintSurface
gestore termina visualizzando un MatrixDisplay
oggetto che mostra la matrice di tocco accumulata:
Modifica di più bitmap
Uno dei vantaggi dell'isolamento del codice di elaborazione tramite tocco in classi come TouchManipulationBitmap
e TouchManipulationManager
è la possibilità di riutilizzare queste classi in un programma che consente all'utente di modificare più bitmap.
La pagina Visualizzazione a dispersione bitmap mostra come viene eseguita questa operazione. Anziché definire un campo di tipo TouchManipulationBitmap
, la BitmapScatterPage
classe definisce un List
oggetto di oggetti 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;
}
}
}
}
...
}
Il costruttore carica tutte le bitmap disponibili come risorse incorporate e le aggiunge a bitmapCollection
. Si noti che la Matrix
proprietà viene inizializzata in ogni TouchManipulationBitmap
oggetto, pertanto gli angoli superiori sinistro di ogni bitmap sono sfalsati di 100 pixel.
La BitmapScatterView
pagina deve anche gestire gli eventi di tocco per più bitmap. Invece di definire un List
di ID tocco di oggetti attualmente modificati TouchManipulationBitmap
, questo programma richiede un dizionario:
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;
}
}
...
}
Si noti che la Pressed
logica scorre l'oggetto bitmapCollection
inverso. Le bitmap spesso si sovrappongono tra loro. Le bitmap più avanti nella raccolta si trovano visivamente sopra le bitmap in precedenza nella raccolta. Se ci sono più bitmap sotto il dito che preme sullo schermo, la parte superiore deve essere quella manipolata da quel dito.
Si noti anche che la logica sposta la Pressed
bitmap alla fine della raccolta in modo che si sposta visivamente nella parte superiore della pila di altre bitmap.
Moved
Negli eventi e Released
il TouchAction
gestore chiama il ProcessingTouchEvent
metodo in TouchManipulationBitmap
proprio come nel programma precedente.
Infine, il PaintSurface
gestore chiama il Paint
metodo di ogni TouchManipulationBitmap
oggetto:
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);
}
}
}
Il codice scorre la raccolta e visualizza la pila di bitmap dall'inizio della raccolta alla fine:
Ridimensionamento con un solo dito
Un'operazione di ridimensionamento richiede in genere un movimento di avvicinamento delle dita usando due dita. Tuttavia, è possibile implementare il ridimensionamento con un solo dito facendo spostare gli angoli di una bitmap.
Questa operazione è illustrata nella pagina Scala ad angolo con dita singola. Poiché questo esempio usa un tipo di ridimensionamento leggermente diverso rispetto a quello implementato nella TouchManipulationManager
classe , non usa tale classe o la TouchManipulationBitmap
classe . Invece, tutta la logica di tocco si trova nel file code-behind. Questa è una logica piuttosto più semplice del solito perché tiene traccia di un solo dito alla volta e ignora semplicemente le dita secondarie che potrebbero toccare lo schermo.
La pagina SingleFingerCornerScale.xaml crea un'istanza della SKCanvasView
classe e crea un TouchEffect
oggetto per tenere traccia degli eventi di tocco:
<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>
Il file SingleFingerCornerScalePage.xaml.cs carica una risorsa bitmap dalla directory Media e la visualizza usando un SKMatrix
oggetto definito come 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);
}
···
}
Questo SKMatrix
oggetto viene modificato dalla logica di tocco illustrata di seguito.
Il resto del file code-behind è il TouchEffect
gestore eventi. Inizia convertendo la posizione corrente del dito in un SKPoint
valore. Per il Pressed
tipo di azione, il gestore verifica che nessun altro dito stia toccando lo schermo e che il dito si trova all'interno dei limiti della bitmap.
La parte cruciale del codice è un'istruzione if
che coinvolge due chiamate al Math.Pow
metodo . Questa matematica controlla se la posizione del dito si trova all'esterno di un'ellisse che riempie la bitmap. In tal caso, si tratta di un'operazione di ridimensionamento. Il dito si trova vicino a uno degli angoli della bitmap e viene determinato un punto pivot che è l'angolo opposto. Se il dito si trova all'interno di questa ellisse, si tratta di un'operazione di panoramica regolare:
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;
}
}
}
Il Moved
tipo di azione calcola una matrice corrispondente all'attività di tocco dal momento in cui il dito ha premuto lo schermo fino a questo momento. Concatena tale matrice con la matrice in vigore al momento in cui il dito ha premuto per la prima volta la bitmap. L'operazione di ridimensionamento è sempre relativa all'angolo opposto a quello toccato dal dito.
Per bitmap piccole o oblong, un'ellisse interna potrebbe occupare la maggior parte della bitmap e lasciare piccole aree agli angoli per ridimensionare la bitmap. È possibile preferire un approccio leggermente diverso, nel qual caso è possibile sostituire l'intero if
blocco impostato isScaling
su true
con questo codice:
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);
}
}
Questo codice divide in modo efficace l'area della bitmap in una forma di rombo interno e quattro triangoli agli angoli. Ciò consente ad aree molto più grandi negli angoli di afferrare e ridimensionare la bitmap.