Manipulace dotyků
Použití maticových transformací k implementaci dotykového přetahování, připnutí a otočení
V prostředích s více dotykovými ovládáními, jako jsou například na mobilních zařízeních, uživatelé často používají prsty k manipulaci s objekty na obrazovce. Běžná gesta, jako je přetažení jedním prstem a dvoupístný špendlík, můžou přesouvat a škálovat objekty nebo je dokonce otáčet. Tato gesta se obecně implementují pomocí transformovaných matic a v tomto článku se dozvíte, jak to udělat.
Všechny zde uvedené ukázky používají efekt dotykového Xamarin.Forms sledování uvedený v článku Vyvolání událostí z efektů.
Přetahování a překlad
Jedním z nejdůležitějších aplikací maticových transformací je dotykové zpracování. Jedna SKMatrix
hodnota může konsolidovat řadu dotykových operací.
Při přetahování jedním prstem provede SKMatrix
hodnota překlad. To je znázorněno na stránce Přetažení bitmapy . Soubor XAML vytvoří SKCanvasView
instanci v objektu Xamarin.FormsGrid
. Objekt TouchEffect
byl přidán do Effects
kolekce, která 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>
Teoreticky TouchEffect
by objekt mohl být přidán přímo do Effects
kolekce SKCanvasView
, ale to nefunguje na všech platformách. Vzhledem k tomu, že je stejná SKCanvasView
velikost jako Grid
v této konfiguraci, připojte ji k Grid
práci stejně dobře.
Soubor s kódem se načte v bitmapovém prostředku v jeho konstruktoru a zobrazí ho v obslužné rutině PaintSurface
:
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());
}
}
Bez dalšího kódu SKMatrix
je hodnota vždy identifikovat matici a nemělo by žádný vliv na zobrazení rastrového obrázku. Cílem obslužné OnTouchEffectAction
rutiny nastavené v souboru XAML je změnit maticovou hodnotu tak, aby odrážela manipulace s dotykem.
Obslužná rutina OnTouchEffectAction
začíná převodem Xamarin.FormsPoint
hodnoty na hodnotu SkiaSharp SKPoint
. Jedná se o jednoduchou záležitost škálování na základě vlastností SKCanvasView
Height
(které jsou jednotky nezávislé na Width
zařízení) a CanvasSize
vlastnosti, které jsou v jednotkách 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;
}
}
···
}
Když se prst poprvé dotkne obrazovky, aktivuje se událost typu TouchActionType.Pressed
. Prvním úkolem je určit, zda se prst dotkne rastrového obrázku. Takový úkol se často nazývá hit-testing. V tomto případě lze dosáhnout hit-testování vytvořením SKRect
hodnoty odpovídající rastrový obrázek, použití maticové transformace s MapRect
a pak určit, zda je dotykový bod uvnitř transformovaného obdélníku.
V takovém případě touchId
je pole nastavené na touch ID a umístění prstu se uloží.
TouchActionType.Moved
U události jsou faktory překladu SKMatrix
hodnoty upraveny na základě aktuální pozice prstu a nové pozice prstu. Tato nová pozice se uloží až příště a SKCanvasView
zneplatní se.
Při experimentování s tímto programem mějte na paměti, že rastrový obrázek můžete přetáhnout pouze tehdy, když se prst dotkne oblasti, ve které je rastrový obrázek zobrazen. I když toto omezení není pro tento program velmi důležité, stává se zásadní při manipulaci s více rastrovými obrázky.
Připnutí a škálování
Co se má stát, když se dva prsty dotkne rastrového obrázku? Pokud se dva prsty pohybují paralelně, pravděpodobně chcete, aby se rastrový obrázek posouvat spolu s prsty. Pokud dva prsty provádějí operaci stažení nebo roztažení, můžete chtít, aby se rastrový obrázek otočil (aby se probíral v další části) nebo škáloval. Při škálování rastrového obrázku dává smysl, aby dva prsty zůstaly ve stejných pozicích vzhledem k bitmapě a aby se rastrový obrázek odpovídajícím způsobem škáloval.
Zpracování dvou prstů najednou vypadá složitě, ale mějte na paměti, že TouchAction
obslužná rutina přijímá informace pouze o jednom prstu najednou. Pokud dva prsty manipulují s rastrem, pak u každé události došlo ke změně pozice jednoho prstu, ale druhý se nezměnil. V rastrovém kódu stránky měřítko níže se prst, který nezměnil pozici, nazývá kontingenční bod, protože transformace je relativní k danému bodu.
Jedním z rozdílů mezi tímto programem a předchozím programem je, že je nutné uložit více dotykových ID. Slovník se používá pro tento účel, kde touch ID je klíč slovníku a hodnota slovníku je aktuální pozice tohoto prstu:
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;
}
}
···
}
Zpracování Pressed
akce je téměř stejné jako předchozí program s tím rozdílem, že ID a dotykový bod jsou přidány do slovníku. Položky Released
slovníku a Cancelled
akce odeberou.
Zpracování Moved
akce je ale složitější. Pokud je zapojen pouze jeden prst, zpracování je velmi stejné jako předchozí program. U dvou nebo více prstů musí program také získat informace ze slovníku zahrnujícího prst, který se nepřehybuje. Provede to zkopírováním klíčů slovníku do pole a následným porovnáním prvního klíče s ID pohybujícího se prstem. To umožňuje programu získat bod otáčení odpovídající prstu, který se nepřesuhuje.
V dalším kroku program vypočítá dva vektory nové pozice prstu vzhledem k bodu otáčení a původní pozici prstu vzhledem k bodu otáčení. Poměry těchto vektorů jsou faktory škálování. Vzhledem k tomu, že dělení nulou je možnost, musí být tyto hodnoty kontrolovány pro nekonečné hodnoty nebo hodnoty NaN (nikoli číselné). Pokud je vše v pořádku, transformace škálování se zřetědí s SKMatrix
hodnotou uloženou jako pole.
Při experimentování s touto stránkou si všimnete, že rastrový obrázek můžete přetáhnout jedním nebo dvěma prsty nebo změnit jeho měřítko dvěma prsty. Měřítko je anisotropní, což znamená, že měřítko se může lišit ve vodorovném a svislém směru. To zkresluje poměr stran, ale také umožňuje překlopit rastrový obrázek, aby se zrcadlový obrázek. Můžete také zjistit, že rastrový obrázek můžete zmenšit na nulovou dimenzi a zmizí. V produkčním kódu budete chtít chránit před tím.
Otočení dvěma prsty
Stránka Rastrové otočení umožňuje použít dva prsty pro otočení nebo isotropní měřítko. Rastrový obrázek vždy zachovává správný poměr stran. Použití dvou prstů pro otočení i anisotropní měřítko nefunguje velmi dobře, protože pohyb prstů je velmi podobný pro oba úkoly.
Prvním velkým rozdílem v tomto programu je logika testování hit-testování. Předchozí programy použily metodu Contains
SKRect
k určení, zda je dotykový bod v transformované obdélník, který odpovídá bitmapu. Když ale uživatel pracuje s rastrovým obrázkem, může být rastrový obrázek otočený a SKRect
nemůže správně reprezentovat otočený obdélník. Možná se bojíte, že logika hit-testování musí v takovém případě implementovat poměrně složitou analytickou geometrii.
Je však k dispozici zkratka: Určení, zda bod leží v mezích transformovaného obdélníku, je stejný jako určení, zda inverzní transformovaný bod leží v rámci hranic nepřepraveného obdélníku. To je mnohem jednodušší výpočet a logika může i nadále používat pohodlnou Contains
metodu:
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));
}
···
}
Logika události Moved
začíná stejně jako předchozí program. Dva vektory pojmenované oldVector
a newVector
jsou vypočítány na základě předchozího a aktuálního bodu pohybujícího se prstu a bodu otáčení neovlivujícího prstu. Pak se ale určují úhly těchto vektorů a rozdíl je úhel otáčení.
Měřítko může být také zapojeno, takže starý vektor se otočí na základě úhlu otočení. Relativní velikost těchto dvou vektorů je teď faktorem měřítka. Všimněte si, že stejná scale
hodnota se používá pro horizontální a vertikální škálování tak, aby škálování je isotropní. Pole matrix
se upraví maticí otočení i maticí měřítka.
Pokud vaše aplikace potřebuje implementovat dotykové zpracování pro jeden rastrový obrázek (nebo jiný objekt), můžete přizpůsobit kód z těchto tří ukázek pro vlastní aplikaci. Pokud ale potřebujete implementovat dotykové zpracování pro více rastrových obrázků, budete pravděpodobně chtít zapouzdřovat tyto dotykové operace v jiných třídách.
Zapouzdření operací dotykového ovládání
Stránka Manipulace s dotykem ukazuje manipulaci s dotykem jednoho rastrového obrázku, ale pomocí několika dalších souborů, které zapouzdřují většinu logiky uvedené výše. První z těchto souborů je TouchManipulationMode
výčet, který označuje různé typy manipulace s dotykem implementovaný kódem, který uvidíte:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
je jedním prstem, který je implementován s překladem. Všechny následující možnosti také zahrnují posouvání, ale zahrnují dva prsty: IsotropicScale
je operace stažení, která má za následek rovnoměrné škálování objektu ve vodorovném a svislém směru. AnisotropicScale
umožňuje nerovné škálování.
Tato možnost je určená ScaleRotate
pro změnu velikosti a otočení dvou prsty. Škálování je isotropní. Jak už bylo zmíněno dříve, implementace otočení dvěma prsty s anisotropní škálování je problematické, protože pohyby prstu jsou v podstatě stejné.
Tato ScaleDualRotate
možnost přidá otočení jedním prstem. Když objekt přetáhnete jedním prstem, přetažený objekt se nejprve otočí kolem středu objektu tak, aby se střed objektu čaroval s přetahováním vektoru.
Soubor TouchManipulationPage.xaml obsahuje Picker
členy výčtu 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>
V dolní části je SKCanvasView
buňka a připojená TouchEffect
k jedné buňce Grid
, která ji uzavře.
Soubor TouchManipulationPage.xaml.cs kódu má bitmap
pole, ale není typu SKBitmap
. Typ je TouchManipulationBitmap
(krátce uvidíte třídu):
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;
}
}
...
}
Konstruktor vytvoří TouchManipulationBitmap
instanci objektu, který předá konstruktoru SKBitmap
získanému z vloženého prostředku. Konstruktor končí nastavením Mode
vlastnosti objektu TouchManipulationBitmap
na člena výčtu TouchManipulationMode
TouchManager
.
Obslužná rutina SelectedIndexChanged
pro Picker
také nastaví tuto Mode
vlastnost:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
Obslužná TouchAction
rutina TouchEffect
instance v souboru XAML volá dvě metody s TouchManipulationBitmap
názvem HitTest
a 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;
}
}
...
}
Pokud metoda HitTest
vrátí true
, což znamená, že prst se dotkne obrazovky v oblasti obsazené rastrovým obrázkem , pak se do kolekce přidá TouchIds
touch ID. Toto ID představuje posloupnost dotykových událostí pro daný prst, dokud prst nezvedne z obrazovky. Pokud se rastrový obrázek dotkne více prstů, touchIds
kolekce obsahuje touch ID pro každý prst.
Obslužná rutina TouchAction
také volá ProcessTouchEvent
třídu v TouchManipulationBitmap
. To je místo, kde dochází k některým (ale ne všem) skutečného zpracování dotykového ovládání.
Třída TouchManipulationBitmap
je obálková třída, která SKBitmap
obsahuje kód pro vykreslení rastrového obrázku a zpracování událostí dotykového ovládání. Funguje ve spojení s obecnějším kódem TouchManipulationManager
ve třídě (který uvidíte za chvíli).
Konstruktor TouchManipulationBitmap
uloží SKBitmap
a vytvoří instanci dvou vlastností, TouchManager
vlastnost typu TouchManipulationManager
a Matrix
vlastnost typu 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; }
...
}
Tato Matrix
vlastnost je kumulovaná transformace, která je výsledkem veškeré dotykové aktivity. Jak vidíte, každá dotyková událost se přeloží do matice, která se pak zřetědí s SKMatrix
hodnotou uloženou vlastností Matrix
.
Objekt TouchManipulationBitmap
se nakreslí ve své Paint
metodě. Argument je SKCanvas
objekt. Tato SKCanvas
transformace už může mít použitou transformaci, takže Paint
metoda zřetězí Matrix
vlastnost přidruženou k bitmapě k existující transformaci a obnoví plátno po dokončení:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
Metoda HitTest
vrátí true
, pokud se uživatel dotkne obrazovky v bodě v rámci hranic rastrového obrázku. Používá se logika zobrazená dříve na stránce Otočení rastrového obrázku:
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;
}
...
}
Druhá veřejná metoda TouchManipulationBitmap
je ProcessTouchEvent
. Při zavolání této metody již bylo zjištěno, že dotyková událost patří do této konkrétní bitmapy. Metoda udržuje slovník TouchManipulationInfo
objektů, což je jednoduše předchozí bod a nový bod každého prstu:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Tady je slovník a ProcessTouchEvent
samotná metoda:
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;
}
}
...
}
V událostech Moved
a Released
událostech metoda volá Manipulate
. V těchto případech touchDictionary
obsahuje jeden nebo více TouchManipulationInfo
objektů. touchDictionary
Pokud obsahuje jednu položku, je pravděpodobné, že PreviousPoint
hodnoty jsou NewPoint
nerovné a představují pohyb prstu. Pokud se rastrový obrázek dotkne více prstů, obsahuje slovník více než jednu položku, ale pouze jedna z těchto položek má jiné PreviousPoint
hodnoty a NewPoint
hodnoty. Všechny ostatní mají stejné PreviousPoint
hodnoty a NewPoint
hodnoty.
To je důležité: Metoda Manipulate
může předpokládat, že zpracovává pohyb pouze jednoho prstu. V době tohoto volání se žádný z ostatních prstů nepřehýbá, a pokud skutečně pohybují (jak je pravděpodobné), tyto pohyby budou zpracovány v budoucích voláních Manipulate
.
Metoda Manipulate
nejprve zkopíruje slovník do pole pro usnadnění. Ignoruje cokoli jiného než první dvě položky. Pokud se rastrový obrázek pokouší manipulovat více než dvěma prsty, ostatní se ignorují. Manipulate
je posledním členem 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;
}
}
Pokud jeden prst pracuje s rastrem, Manipulate
volá OneFingerManipulate
metodu objektu TouchManipulationManager
. Pro dva prsty volá TwoFingerManipulate
. Argumenty těchto metod jsou stejné: prevPoint
argumenty newPoint
představují prst, který se přesouvá. pivotPoint
Ale argument se u těchto dvou volání liší:
Pro manipulaci pivotPoint
jedním prstem je střed rastrového obrázku. To umožňuje otočení jedním prstem. U manipulace se dvěma prsty označuje událost pohyb pouze jednoho prstu, takže pivotPoint
je to prst, který se nepřehybuje.
V obou případech TouchManipulationManager
vrátí SKMatrix
hodnotu, která metoda zřetězí s aktuální Matrix
vlastností, která TouchManipulationPage
používá k vykreslení rastrového obrázku.
TouchManipulationManager
je zobecněn a nepoužívá žádné jiné soubory s výjimkou TouchManipulationMode
. Tuto třídu můžete používat beze změny ve vlastních aplikacích. Definuje jednu vlastnost typu TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Pravděpodobně se ale budete chtít této možnosti vyhnout AnisotropicScale
. S touto možností je velmi snadné manipulovat s rastrovým obrázkem, aby se jeden z faktorů škálování stala nulou. Díky tomu rastrový obrázek zmizí z dohledu, nikdy se nevrátí. Pokud skutečně potřebujete anisotropní škálování, budete chtít vylepšit logiku, abyste se vyhnuli nežádoucím výsledkům.
TouchManipulationManager
používá vektory, ale vzhledem k tomu, že v SkiaSharpu není žádná SKVector
struktura, SKPoint
se místo toho používá. SKPoint
podporuje operátor odčítání a výsledek lze považovat za vektor. Jedinou vektorovou logikou Magnitude
, kterou je potřeba přidat, je výpočet:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Pokaždé, když je vybráno otočení, zpracují se jako první metody manipulace s jedním prstem i dvěma prsty. Pokud se zjistí nějaká rotace, komponenta rotace se efektivně odebere. To, co zůstává, se interpretuje jako posouvání a škálování.
Tady je OneFingerManipulate
metoda. Pokud není povoleno otočení jedním prstem, je logika jednoduchá – jednoduše používá předchozí bod a nový bod k vytvoření vektoru s názvem delta
, který přesně odpovídá překladu. Při povoleném otočení jedním prstem používá metoda úhely z bodu otáčení (střed rastrového obrázku) na předchozí bod a nový bod k vytvoření matice otočení:
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
V metodě je kontingenční bod pozice prstu, který se v této konkrétní události dotykového ovládání nepřesuhuje. Otočení je velmi podobné otočení jedním prstem a vektor pojmenovaný oldVector
(na základě předchozího bodu) se upraví pro otočení. Zbývající přesun se interpretuje jako škálování:
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;
}
...
}
Všimněte si, že tato metoda neobsahuje explicitní překlad. Obě metody MakeRotation
MakeScale
jsou však založeny na kontingenčním bodu a zahrnují implicitní překlad. Pokud na rastrovém obrázku používáte dva prsty a přetáhnete je stejným směrem, získáte řadu dotykových událostí, TouchManipulation
které se střídají mezi dvěma prsty. Když se každý prst pohybuje vzhledem k druhému, škálování nebo otočení výsledků, ale je negovaný pohybem druhého prstu a výsledek je překlad.
Jedinou zbývající částí stránky Manipulace s dotykovým ovládáním PaintSurface
je obslužná rutina v TouchManipulationPage
souboru kódu. To volá Paint
metodu TouchManipulationBitmap
, která aplikuje matici představující kumulovanou dotykovou aktivitu:
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));
}
}
Obslužná rutina PaintSurface
končí zobrazením objektu zobrazující kumulovanou dotykovou MatrixDisplay
matici:
Manipulace s více rastrovými obrázky
Jednou z výhod izolace kódu dotykového zpracování ve třídách, jako TouchManipulationBitmap
TouchManipulationManager
je a je schopnost opakovaně používat tyto třídy v programu, který uživateli umožňuje manipulovat s více rastrovými obrázky.
Stránka Rastrové bodové zobrazení ukazuje, jak se to dělá. Místo definování pole typu TouchManipulationBitmap
BitmapScatterPage
třída definuje List
rastrové objekty:
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;
}
}
}
}
...
}
Konstruktor se načte do všech rastrových obrázků, které jsou k dispozici jako vložené prostředky, a přidá je do bitmapCollection
. Všimněte si, že Matrix
vlastnost je inicializována u každého TouchManipulationBitmap
objektu, takže levé horní rohy každého rastrového obrázku jsou posunuty o 100 pixelů.
Stránka BitmapScatterView
také potřebuje zpracovat dotykové události pro více rastrových obrázků. Místo definování List
dotykových ID aktuálně manipulovaných TouchManipulationBitmap
objektů tento program vyžaduje slovník:
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;
}
}
...
}
Všimněte si, jak Pressed
logika prochází obráceně bitmapCollection
. Rastrové obrázky se často vzájemně překrývají. Rastrové obrázky později v kolekci vizuálně leží nad rastrovými obrázky dříve v kolekci. Pokud je pod prstem více rastrových obrázků, které se stisknou na obrazovce, musí být první rastrový obrázek, který je manipulován tímto prstem.
Všimněte si také, že logika Pressed
přesune rastrový obrázek na konec kolekce, aby se vizuálně přesunula na začátek hromady jiných rastrových obrázků.
Moved
V událostech a Released
obslužná rutina TouchAction
volá metodu ProcessingTouchEvent
stejně TouchManipulationBitmap
jako předchozí program.
Nakonec obslužná rutina PaintSurface
volá metodu každého TouchManipulationBitmap
objektuPaint
:
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);
}
}
}
Kód prochází kolekcí a zobrazuje hromádku rastrových obrázků od začátku kolekce až po konec:
Měřítko jedním prstem
Operace škálování obecně vyžaduje gesto stažení dvěma prsty. Škálování je však možné implementovat jedním prstem tak, že prstem posune rohy rastrového obrázku.
To je znázorněno na stránce Měřítko s jedním prstem. Vzhledem k tomu, že tato ukázka používá poněkud jiný typ škálování než implementovaný ve TouchManipulationManager
třídě, nepoužívá tuto třídu ani TouchManipulationBitmap
třídu. Místo toho je veškerá dotyková logika v souboru kódu. To je poněkud jednodušší logika než obvykle, protože sleduje vždy jenom jeden prst a jednoduše ignoruje všechny sekundární prsty, které se můžou dotýkat obrazovky.
Stránka SingleFingerCornerScale.xaml vytvoří SKCanvasView
instanci třídy a vytvoří TouchEffect
objekt pro sledování dotykových událostí:
<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>
Soubor SingleFingerCornerScalePage.xaml.cs načte rastrový prostředek z adresáře Médií a zobrazí ho pomocí objektu SKMatrix
definovaného jako pole:
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);
}
···
}
Tento SKMatrix
objekt je upraven dotykovou logikou zobrazenou níže.
Zbytek souboru kódu je obslužná rutina TouchEffect
události. Začíná převodem aktuálního umístění prstu na SKPoint
hodnotu. Pressed
U typu akce obslužná rutina zkontroluje, že se na obrazovce nedotýká žádný jiný prst a že je prst v mezích rastrového obrázku.
Klíčovou součástí kódu je if
příkaz zahrnující dvě volání Math.Pow
metody. Tato matematika zkontroluje, jestli je umístění prstu mimo tři tečky, které vyplní rastrový obrázek. Pokud ano, znamená to operace škálování. Prst je blízko jednoho z rohů rastrového obrázku a bod otáčení je určen, že je opačný roh. Pokud je prst v tomto elipse, jedná se o běžnou operaci posouvání:
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;
}
}
}
Typ Moved
akce vypočítá matici odpovídající dotykové aktivitě od okamžiku, kdy prst po stisknutí prstu až do této doby stiskl obrazovku. Zřetězí matici s maticí v okamžiku, kdy prst poprvé stiskl rastrový obrázek. Operace škálování je vždy relativní vzhledem k rohu proti rohu, na který se prst dotkl.
U malých nebo podlouhlých rastrových obrázků může vnitřní elipsa zabírat většinu rastrového obrázku a nechat malé oblasti v rozích, aby bylo možné rastrový obrázek škálovat. Můžete preferovat poněkud jiný přístup, v takovém případě můžete nahradit celý if
blok, který se nastaví isScaling
na true
tento kód:
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);
}
}
Tento kód efektivně rozdělí oblast rastrového obrázku na vnitřní kosočtverec a čtyři trojúhelníky v rozích. To umožňuje mnohem větší oblasti v rozích zachytit a škálovat rastrový obrázek.