Manipulationen durch Toucheingaben
Verwenden von Matrixtransformationen zum Implementieren von Fingereingaben, Zusammendrücken und Drehungen
In Multitouchumgebungen wie z. B. auf mobilen Geräten verwenden Benutzer häufig ihre Finger, um Objekte auf dem Bildschirm zu bearbeiten. Häufige Gesten wie ein Fingerziehvorgang und ein Zusammendrücken mit zwei Fingern können Objekte verschieben und skalieren oder sogar drehen. Diese Gesten werden in der Regel mithilfe von Transformationsmatrizen implementiert, und in diesem Artikel wird erläutert, wie Sie dies tun.
Alle hier gezeigten Beispiele verwenden den Xamarin.Forms Touchverfolgungseffekt, der im Artikel "Ereignisse aus Effekten aufrufen" dargestellt wird.
Ziehen und Übersetzen
Eine der wichtigsten Anwendungen von Matrixtransformationen ist die Touchverarbeitung. Ein einzelner SKMatrix
Wert kann eine Reihe von Fingereingabevorgängen konsolidieren.
Beim Ziehen mit einem Finger führt der SKMatrix
Wert übersetzungen aus. Dies wird auf der Bitmap-Ziehseite veranschaulicht. Die XAML-Datei instanziiert eine SKCanvasView
in einer Xamarin.FormsGrid
. Der Auflistung dieses Grid
Objekts wurde ein TouchEffect
Objekt hinzugefügtEffects
:
<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>
Theoretisch könnte das TouchEffect
Objekt direkt zur Effects
Auflistung der SKCanvasView
, aber nicht auf allen Plattformen verwendet werden. Da die SKCanvasView
Gleiche Größe wie die Grid
in dieser Konfiguration ist, fügen Sie sie ebenfalls an die Grid
Funktionsweise an.
Die CodeBehind-Datei wird in einer Bitmapressource im Konstruktor geladen und im PaintSurface
Handler angezeigt:
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());
}
}
Ohne weiteren Code ist der SKMatrix
Wert immer die Identifikationsmatrix, und er hat keine Auswirkungen auf die Anzeige der Bitmap. Das Ziel des Handlers, der OnTouchEffectAction
in der XAML-Datei festgelegt ist, besteht darin, den Matrixwert so zu ändern, dass er Die Fingereingabemanipulationen widerspiegelt.
Der OnTouchEffectAction
Handler beginnt mit der Konvertierung des Xamarin.FormsPoint
Werts in einen SkiaSharp-Wert SKPoint
. Dies ist eine einfache Angelegenheit der Skalierung basierend auf den Width
Und Eigenschaften von SKCanvasView
(die geräteunabhängige Einheiten sind) und der CanvasSize
Eigenschaft, die sich in Einheiten von Height
Pixeln befindet:
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;
}
}
···
}
Wenn ein Finger zuerst den Bildschirm berührt, wird ein Ereignis vom Typ TouchActionType.Pressed
ausgelöst. Die erste Aufgabe besteht darin, zu bestimmen, ob der Finger die Bitmap berührt. Eine solche Aufgabe wird häufig als Treffertests bezeichnet. In diesem Fall können Treffertests durchgeführt werden, indem ein SKRect
Wert erstellt wird, der der Bitmap entspricht, die Matrixtransformation auf MapRect
sie angewendet und dann ermittelt wird, ob sich der Touchpunkt innerhalb des transformierten Rechtecks befindet.
Wenn dies der Fall ist, wird das touchId
Feld auf die Touch-ID festgelegt, und die Fingerposition wird gespeichert.
Für das TouchActionType.Moved
Ereignis werden die Übersetzungsfaktoren des SKMatrix
Werts basierend auf der aktuellen Position des Fingers und der neuen Position des Fingers angepasst. Diese neue Position wird zum nächsten Mal bis zum nächsten Mal gespeichert, und die SKCanvasView
Position wird ungültig.
Beachten Sie beim Experimentieren mit diesem Programm, dass Sie die Bitmap nur ziehen können, wenn der Finger einen Bereich berührt, in dem die Bitmap angezeigt wird. Obwohl diese Einschränkung für dieses Programm nicht sehr wichtig ist, wird es beim Bearbeiten mehrerer Bitmaps entscheidend.
Zusammendrücken und Skalieren
Was soll passieren, wenn zwei Finger die Bitmap berühren? Wenn sich die beiden Finger parallel bewegen, soll die Bitmap wahrscheinlich mit den Fingern verschoben werden. Wenn die beiden Finger einen Zusammendrücken oder Streckenvorgang ausführen, möchten Sie möglicherweise, dass die Bitmap gedreht (im nächsten Abschnitt erläutert) oder skaliert werden soll. Beim Skalieren einer Bitmap ist es am sinnvollsten, dass die beiden Finger in den gleichen Positionen relativ zur Bitmap wieder Standard und damit die Bitmap entsprechend skaliert werden kann.
Das Behandeln von zwei Fingern auf einmal scheint kompliziert, aber denken Sie daran, dass der TouchAction
Handler nur Jeweils Informationen über einen Finger empfängt. Wenn zwei Finger die Bitmap bearbeiten, hat sich für jedes Ereignis die Position eines Fingers geändert, aber das andere hat sich nicht geändert. Im code der Bitmapskalierungsseite unten wird der Finger, der die Position nicht geändert hat, als Pivotpunkt bezeichnet, da die Transformation relativ zu diesem Punkt ist.
Ein Unterschied zwischen diesem Programm und dem vorherigen Programm besteht darin, dass mehrere Touch-IDs gespeichert werden müssen. Ein Wörterbuch wird zu diesem Zweck verwendet, wobei die Touch-ID die Wörterbuchtaste ist und der Wörterbuchwert die aktuelle Position dieses Fingers ist:
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;
}
}
···
}
Die Behandlung der Pressed
Aktion ist fast identisch mit dem vorherigen Programm, mit der Ausnahme, dass die ID und der Touchpunkt dem Wörterbuch hinzugefügt werden. Die Released
Und Cancelled
Aktionen entfernen den Wörterbucheintrag.
Die Behandlung der Moved
Aktion ist jedoch komplexer. Wenn nur ein Finger beteiligt ist, ist die Verarbeitung sehr ähnlich wie das vorherige Programm. Bei zwei oder mehr Fingern muss das Programm auch Informationen aus dem Wörterbuch mit dem Finger abrufen, der nicht bewegt wird. Dazu kopieren Sie die Wörterbuchschlüssel in ein Array, und vergleichen Sie dann den ersten Schlüssel mit der ID des Fingers, der verschoben wird. Dadurch kann das Programm den Pivotpunkt abrufen, der dem Finger entspricht, der nicht bewegt wird.
Als Nächstes berechnet das Programm zwei Vektoren der neuen Fingerposition relativ zum Pivotpunkt und die alte Fingerposition relativ zum Pivotpunkt. Die Verhältnisse dieser Vektoren sind Skalierungsfaktoren. Da die Division durch Null eine Möglichkeit ist, müssen diese auf unendliche Werte oder NaN -Werte (keine Zahl) überprüft werden. Wenn alles gut ist, wird eine Skalierungstransformation mit dem SKMatrix
als Feld gespeicherten Wert verkettet.
Während Sie mit dieser Seite experimentieren, werden Sie feststellen, dass Sie die Bitmap mit einem oder zwei Fingern ziehen oder mit zwei Fingern skalieren können. Die Skalierung ist anisotrop, was bedeutet, dass die Skalierung in horizontaler und vertikaler Richtung unterschiedlich sein kann. Dadurch wird das Seitenverhältnis verzerrt, sie können aber auch die Bitmap kippen, um ein Spiegel Bild zu erstellen. Möglicherweise stellen Sie auch fest, dass Sie die Bitmap auf eine Nulldimension verkleinern können, und sie verschwindet. Im Produktionscode sollten Sie sich davor schützen.
Drehung mit zwei Fingern
Mit der Bitmapdrehungsseite können Sie zwei Finger für die Drehung oder isotrope Skalierung verwenden. Die Bitmap behält immer das richtige Seitenverhältnis bei. Die Verwendung von zwei Fingern für die Drehung und die anisotrope Skalierung funktioniert nicht sehr gut, da die Bewegung der Finger für beide Aufgaben sehr ähnlich ist.
Der erste große Unterschied in diesem Programm ist die Treffertestlogik. Die vorherigen Programme haben die Contains
Methode SKRect
verwendet, um zu bestimmen, ob sich der Touchpunkt innerhalb des transformierten Rechtecks befindet, das der Bitmap entspricht. Während der Benutzer die Bitmap bearbeitet, wird die Bitmap möglicherweise gedreht und SKRect
kann kein gedrehtes Rechteck richtig darstellen. Sie könnten befürchten, dass die Treffertestlogik in diesem Fall ziemlich komplexe Analysegeometrie implementieren muss.
Es ist jedoch eine Verknüpfung verfügbar: Ermitteln, ob ein Punkt innerhalb der Grenzen eines transformierten Rechtecks liegt, ist identisch mit der Bestimmung, ob ein umgekehrter transformierter Punkt innerhalb der Grenzen des untransformierten Rechtecks liegt. Dies ist eine viel einfachere Berechnung, und die Logik kann weiterhin die bequeme Contains
Methode verwenden:
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));
}
···
}
Die Logik für das Moved
Ereignis beginnt wie das vorherige Programm. Zwei benannte oldVector
Vektoren und newVector
werden basierend auf dem vorherigen und dem aktuellen Punkt des bewegenden Fingers und dem Pivotpunkt des ungeovten Fingers berechnet. Dann werden jedoch Winkel dieser Vektoren bestimmt, und der Unterschied ist der Drehwinkel.
Die Skalierung kann auch beteiligt sein, sodass der alte Vektor basierend auf dem Drehwinkel gedreht wird. Die relative Größe der beiden Vektoren ist nun der Skalierungsfaktor. Beachten Sie, dass derselbe scale
Wert für die horizontale und vertikale Skalierung verwendet wird, sodass die Skalierung isotropisch ist. Das matrix
Feld wird sowohl durch die Drehungsmatrix als auch durch eine Skalierungsmatrix angepasst.
Wenn Ihre Anwendung die Touchverarbeitung für eine einzelne Bitmap (oder ein anderes Objekt) implementieren muss, können Sie den Code aus diesen drei Beispielen für Ihre eigene Anwendung anpassen. Wenn Sie jedoch die Touchverarbeitung für mehrere Bitmaps implementieren müssen, sollten Sie diese Fingereingabevorgänge wahrscheinlich in anderen Klassen kapseln.
Kapselung der Toucheingabevorgänge
Auf der Seite "Touchbearbeitung " wird die Fingereingabebearbeitung einer einzelnen Bitmap veranschaulicht, wobei jedoch mehrere andere Dateien verwendet werden, die einen Großteil der oben gezeigten Logik kapseln. Die erste dieser Dateien ist die TouchManipulationMode
Aufzählung, die die verschiedenen Arten der Touchbearbeitung angibt, die vom Code implementiert werden, den Sie sehen werden:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
ist ein Ziehvorgang mit einem Finger, der mit Übersetzung implementiert wird. Alle nachfolgenden Optionen umfassen auch die Verschiebung, umfassen jedoch zwei Finger: IsotropicScale
Eine Zusammendrückoperation, die zu einer gleichmäßigen Skalierung des Objekts in horizontaler und vertikaler Richtung führt. AnisotropicScale
ermöglicht eine ungleiche Skalierung.
Die ScaleRotate
Option ist für die Skalierung und Drehung mit zwei Fingern. Die Skalierung ist isotrop. Wie zuvor Erwähnung, ist die Implementierung der Zweifingerdrehung mit anisotroper Skalierung problematisch, da die Fingerbewegungen im Wesentlichen gleich sind.
Die ScaleDualRotate
Option fügt eine Drehung mit einem Finger hinzu. Wenn ein einzelner Finger das Objekt zieht, wird das gezogene Objekt zuerst um seine Mitte gedreht, sodass sich die Mitte des Objekts mit dem Ziehvektor nach oben richtet.
Die Datei "TouchManipulationPage.xaml" enthält eine Picker
mit den Membern der TouchManipulationMode
Enumeration:
<?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>
In Richtung unten befindet sich eine SKCanvasView
und eine TouchEffect
an die einzelne Zelle angefügte Zelle Grid
, die sie umschließt.
Die TouchManipulationPage.xaml.cs CodeBehind-Datei weist ein bitmap
Feld auf, ist jedoch nicht vom Typ SKBitmap
. Der Typ ist TouchManipulationBitmap
(ein Kurs, den Sie in Kürze sehen):
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;
}
}
...
}
Der Konstruktor instanziiert ein TouchManipulationBitmap
Objekt und übergibt an den Konstruktor eine SKBitmap
aus einer eingebetteten Ressource abgerufene Ressource. Der Konstruktor endet, indem die Mode
Eigenschaft der TouchManager
Eigenschaft des TouchManipulationBitmap
Objekts auf ein Element der TouchManipulationMode
Enumeration festgelegt wird.
Der SelectedIndexChanged
Handler für die Picker
Eigenschaft legt Mode
diese Eigenschaft fest:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
Der TouchAction
Handler der TouchEffect
instanziierten XAML-Datei ruft zwei Methoden in TouchManipulationBitmap
benannter HitTest
Und 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;
}
}
...
}
Wenn die HitTest
Methode zurückgegeben wird true
, d. h., ein Finger hat den Bildschirm innerhalb des Bereichs berührt, der von der Bitmap belegt wird, wird die Touch-ID der TouchIds
Auflistung hinzugefügt. Diese ID stellt die Abfolge von Touchereignissen für diesen Finger dar, bis der Finger vom Bildschirm aufhebt. Wenn mehrere Finger die Bitmap berühren, enthält die touchIds
Auflistung eine Touch-ID für jeden Finger.
Der TouchAction
Handler ruft auch die ProcessTouchEvent
Klasse in TouchManipulationBitmap
. Dies ist der Ort, an dem einige (aber nicht alle) der echten Fingereingabeverarbeitung auftreten.
Die TouchManipulationBitmap
Klasse ist eine Wrapperklasse, die SKBitmap
Code zum Rendern der Bitmap- und Verarbeitungsereignisse enthält. Es funktioniert in Verbindung mit allgemeinerem Code in einer TouchManipulationManager
Klasse (die Sie in Kürze sehen).
Der TouchManipulationBitmap
Konstruktor speichert und SKBitmap
instanziiert zwei Eigenschaften, die TouchManager
Eigenschaft vom Typ TouchManipulationManager
und die Matrix
Eigenschaft des Typs 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; }
...
}
Diese Matrix
Eigenschaft ist die angesammelte Transformation, die sich aus allen Touchaktivitäten ergibt. Wie Sie sehen, wird jedes Touchereignis in eine Matrix aufgelöst, die dann mit dem SKMatrix
von der Matrix
Eigenschaft gespeicherten Wert verkettet wird.
Das TouchManipulationBitmap
Objekt zeichnet sich selbst in seiner Paint
Methode. Das Argument ist ein SKCanvas
Objekt. Dies SKCanvas
kann bereits eine Transformation darauf angewendet haben, sodass die Methode die Paint
mit der Matrix
Bitmap verknüpfte Eigenschaft mit der vorhandenen Transformation verkettet und den Zeichenbereich wiederhergestellt, wenn sie fertig ist:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
Die HitTest
Methode gibt zurück true
, wenn der Benutzer den Bildschirm an einem Punkt innerhalb der Begrenzungen der Bitmap berührt. Dies verwendet die Logik, die weiter oben auf der Bitmapdrehungsseite angezeigt wird:
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;
}
...
}
Die zweite öffentliche Methode in TouchManipulationBitmap
ist ProcessTouchEvent
. Wenn diese Methode aufgerufen wird, wurde bereits festgestellt, dass das Touchereignis zu dieser bestimmten Bitmap gehört. Die Methode Standard enthält ein Wörterbuch von TouchManipulationInfo
Objekten, das einfach der vorherige Punkt und der neue Punkt jedes Fingers ist:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Hier sehen Sie das Wörterbuch und die ProcessTouchEvent
Methode selbst:
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;
}
}
...
}
In den Moved
Und Released
Ereignissen ruft die Methode auf Manipulate
. Zu diesen Zeiten enthält das touchDictionary
Objekt ein oder mehrere TouchManipulationInfo
Objekte. Wenn das touchDictionary
Element ein Element enthält, ist es wahrscheinlich, dass die PreviousPoint
Werte NewPoint
ungleich sind und die Bewegung eines Fingers darstellen. Wenn mehrere Finger die Bitmap berühren, enthält das Wörterbuch mehrere Elemente, aber nur eines dieser Elemente weist unterschiedliche PreviousPoint
Werte auf NewPoint
. Der rest hat gleich PreviousPoint
und NewPoint
werte.
Dies ist wichtig: Die Manipulate
Methode kann davon ausgehen, dass sie die Bewegung nur mit einem Finger verarbeitet. Zum Zeitpunkt dieses Aufrufs bewegen sich keine der anderen Finger, und wenn sie sich wirklich bewegen (wie wahrscheinlich), werden diese Bewegungen in zukünftigen Aufrufen Manipulate
verarbeitet.
Die Manipulate
Methode kopiert das Wörterbuch zunächst aus Gründen der Einfachheit in ein Array. Es ignoriert alles andere als die ersten beiden Einträge. Wenn mehr als zwei Finger versuchen, die Bitmap zu bearbeiten, werden die anderen ignoriert. Manipulate
ist das letzte Mitglied von 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;
}
}
Wenn ein Finger die Bitmap bearbeitet, Manipulate
wird die OneFingerManipulate
Methode des TouchManipulationManager
Objekts aufgerufen. Für zwei Finger ruft TwoFingerManipulate
es . Die Argumente für diese Methoden sind identisch: Die prevPoint
Argumente und newPoint
Argumente stellen den Finger dar, der sich bewegt. Das pivotPoint
Argument unterscheidet sich jedoch für die beiden Aufrufe:
Bei der Manipulation mit einem Finger ist die pivotPoint
Mitte der Bitmap. Dies ist die Möglichkeit der Drehung mit einem Finger. Bei der Manipulation mit zwei Fingern gibt das Ereignis die Bewegung nur eines Fingers an, sodass es sich pivotPoint
um den Finger handelt, der nicht bewegt wird.
In beiden Fällen TouchManipulationManager
wird ein SKMatrix
Wert zurückgegeben, der von der Methode mit der aktuellen Matrix
Eigenschaft verkettet wird, die TouchManipulationPage
zum Rendern der Bitmap verwendet wird.
TouchManipulationManager
ist generalisiert und verwendet keine anderen Dateien außer TouchManipulationMode
. Möglicherweise können Sie diese Klasse ohne Änderung in Ihren eigenen Anwendungen verwenden. Sie definiert eine einzelne Eigenschaft vom Typ TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Sie sollten die AnisotropicScale
Option jedoch wahrscheinlich vermeiden. Mit dieser Option ist es sehr einfach, die Bitmap zu bearbeiten, sodass einer der Skalierungsfaktoren null wird. Dadurch wird die Bitmap nicht mehr sichtbar, und sie kann niemals zurückgegeben werden. Wenn Sie wirklich anisotrope Skalierung benötigen, sollten Sie die Logik verbessern, um unerwünschte Ergebnisse zu vermeiden.
TouchManipulationManager
verwendet Vektoren, aber da es keine SKVector
Struktur in SkiaSharp gibt, SKPoint
wird stattdessen verwendet. SKPoint
unterstützt den Subtraktionsoperator, und das Ergebnis kann als Vektor behandelt werden. Die einzige vektorspezifische Logik, die hinzugefügt werden muss, ist eine Magnitude
Berechnung:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Wenn die Drehung ausgewählt wurde, behandeln die Manipulationsmethoden mit einem Finger und zwei Fingern zuerst die Drehung. Wenn eine Drehung erkannt wird, wird die Drehungskomponente effektiv entfernt. Was neu Standard wird als Verschiebung und Skalierung interpretiert.
Dies ist die OneFingerManipulate
Methode. Wenn die Drehung mit einem Finger nicht aktiviert wurde, ist die Logik einfach – sie verwendet einfach den vorherigen Punkt und neuen Punkt, um einen Vektor delta
zu erstellen, der genau der Übersetzung entspricht. Wenn die Drehung mit einem Finger aktiviert ist, verwendet die Methode Winkel vom Pivotpunkt (die Mitte der Bitmap) zum vorherigen Punkt und neuen Punkt, um eine Drehungsmatrix zu erstellen:
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;
}
...
}
In der TwoFingerManipulate
Methode ist der Pivotpunkt die Position des Fingers, die sich in diesem bestimmten Touchereignis nicht bewegt. Die Drehung ist der Drehung mit einem Finger sehr ähnlich, und dann wird der benannte oldVector
Vektor (basierend auf dem vorherigen Punkt) für die Drehung angepasst. Die Um Standard bewegung wird als Skalierung interpretiert:
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;
}
...
}
Sie werden feststellen, dass in dieser Methode keine explizite Übersetzung vorhanden ist. Sowohl die Methoden als MakeScale
auch die MakeRotation
Methoden basieren jedoch auf dem Pivotpunkt und enthalten implizite Übersetzungen. Wenn Sie zwei Finger auf der Bitmap verwenden und in die gleiche Richtung ziehen, TouchManipulation
wird eine Reihe von Touchereignissen zwischen den beiden Fingern wechselt. Wenn sich jeder Finger relativ zum anderen bewegt, führt die Skalierung oder Drehung zu den Ergebnissen, wird jedoch von der Bewegung des anderen Fingers negiert, und das Ergebnis wird übersetzt.
Der Standard einzige teil der Seite "Touchbearbeitung" ist der PaintSurface
Handler in der TouchManipulationPage
CodeBehind-Datei. Dadurch wird die Paint
Methode des TouchManipulationBitmap
, die die Matrix anwendet, die die akkumulierte Touchaktivität darstellt:
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));
}
}
Der PaintSurface
Handler wird beendet, indem ein MatrixDisplay
Objekt mit der angesammelten Touchmatrix angezeigt wird:
Bearbeiten mehrerer Bitmaps
Einer der Vorteile beim Isolieren von Touchverarbeitungscode in Klassen wie TouchManipulationBitmap
und TouchManipulationManager
die Möglichkeit, diese Klassen in einem Programm wiederzuverwenden, mit dem der Benutzer mehrere Bitmaps bearbeiten kann.
Auf der Seite "Bitmap-Punktansicht " wird gezeigt, wie dies erfolgt. Anstatt ein Feld vom Typ TouchManipulationBitmap
zu definieren, definiert die BitmapScatterPage
Klasse ein List
Bitmapobjekt:
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;
}
}
}
}
...
}
Der Konstruktor wird in allen Bitmaps geladen, die als eingebettete Ressourcen verfügbar sind, und fügt sie der bitmapCollection
Datei hinzu. Beachten Sie, dass die Matrix
Eigenschaft für jedes TouchManipulationBitmap
Objekt initialisiert wird, sodass die oberen linken Ecken jeder Bitmap um 100 Pixel versetzt werden.
Die BitmapScatterView
Seite muss auch Touchereignisse für mehrere Bitmaps behandeln. Anstatt eine List
Touch-IDs von aktuell bearbeiteten TouchManipulationBitmap
Objekten zu definieren, erfordert dieses Programm ein Wörterbuch:
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;
}
}
...
}
Beachten Sie, wie die Pressed
Logik umgekehrt durchläuft bitmapCollection
. Die Bitmaps überlappen sich häufig gegenseitig. Die Bitmaps weiter unten in der Auflistung befinden sich visuell über den Bitmaps weiter oben in der Auflistung. Wenn mehrere Bitmaps unter dem Finger vorhanden sind, die auf dem Bildschirm gedrückt werden, muss die oberste Bitmap die von diesem Finger bearbeitete sein.
Beachten Sie außerdem, dass die Pressed
Logik diese Bitmap an das Ende der Auflistung verschiebt, sodass sie visuell an den Anfang des Stapels anderer Bitmaps verschoben wird.
In den Moved
Und-Ereignissen Released
ruft der TouchAction
Handler die ProcessingTouchEvent
Methode TouchManipulationBitmap
genau wie das frühere Programm auf.
Schließlich ruft der PaintSurface
Handler die Paint
Methode der einzelnen TouchManipulationBitmap
Objekte auf:
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);
}
}
}
Der Code durchläuft die Auflistung und zeigt den Stapel von Bitmaps vom Anfang der Auflistung bis zum Ende an:
Skalierung mit einem Finger
Bei einem Skalierungsvorgang ist in der Regel eine Zusammendrückbewegung mit zwei Fingern erforderlich. Es ist jedoch möglich, die Skalierung mit einem einzelnen Finger zu implementieren, indem der Finger die Ecken einer Bitmap bewegt.
Dies wird auf der Seite "Single Finger Corner Scale " veranschaulicht. Da in diesem Beispiel eine etwas andere Art von Skalierung verwendet wird als die in der TouchManipulationManager
Klasse implementierte, wird diese Klasse oder die TouchManipulationBitmap
Klasse nicht verwendet. Stattdessen befindet sich die gesamte Touchlogik in der CodeBehind-Datei. Dies ist etwas einfachere Logik als üblich, da sie nur einen Finger gleichzeitig verfolgt und einfach alle sekundären Finger ignoriert, die möglicherweise den Bildschirm berühren.
Die SingleFingerCornerScale.xaml-Seite instanziiert die SKCanvasView
Klasse und erstellt ein TouchEffect
Objekt zum Nachverfolgen von Touchereignissen:
<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>
Die SingleFingerCornerScalePage.xaml.cs Datei lädt eine Bitmapressource aus dem Medienverzeichnis und zeigt sie mithilfe eines SKMatrix
Objekts an, das als Feld definiert ist:
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);
}
···
}
Dieses SKMatrix
Objekt wird von der unten gezeigten Touchlogik geändert.
Der re Standard der der CodeBehind-Datei ist der TouchEffect
Ereignishandler. Zunächst wird die aktuelle Position des Fingers in einen SKPoint
Wert konvertiert. Für den Pressed
Aktionstyp überprüft der Handler, ob kein anderer Finger den Bildschirm berührt, und dass sich der Finger innerhalb der Grenzen der Bitmap befindet.
Der entscheidende Teil des Codes ist eine if
Anweisung mit zwei Aufrufen der Math.Pow
Methode. Diese Mathematik überprüft, ob sich die Fingerposition außerhalb einer Ellipse befindet, die die Bitmap ausfüllt. Wenn ja, ist dies ein Skalierungsvorgang. Der Finger befindet sich in der Nähe einer der Ecken der Bitmap, und ein Pivotpunkt wird bestimmt, dass es sich um die gegenüberliegende Ecke handelt. Wenn sich der Finger innerhalb dieser Ellipse befindet, handelt es sich um einen normalen Verschiebungsvorgang:
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;
}
}
}
Der Moved
Aktionstyp berechnet eine Matrix, die der Touchaktivität entspricht, ab dem Zeitpunkt, zu dem der Finger den Bildschirm bis zu diesem Zeitpunkt gedrückt hat. Sie verkettet diese Matrix mit der tatsächlichen Matrix, wenn der Finger zuerst die Bitmap gedrückt hat. Der Skalierungsvorgang ist immer relativ zur Ecke gegenüber dem, den der Finger berührt hat.
Bei kleinen oder länglichen Bitmaps belegt eine innere Ellipse möglicherweise den größten Teil der Bitmap und lassen winzige Bereiche an den Ecken, um die Bitmap zu skalieren. Möglicherweise bevorzugen Sie einen etwas anderen Ansatz, in diesem Fall können Sie diesen gesamten if
Block ersetzen, der durch diesen Code festgelegt wird isScaling
true
:
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);
}
}
Dieser Code teilt den Bereich der Bitmap effektiv in eine innere Rautenform und vier Dreiecke an den Ecken auf. Dies ermöglicht es viel größeren Bereichen an den Ecken, die Bitmap zu erfassen und zu skalieren.