Manipulacje za pomocą dotyku
Używanie przekształceń macierzy do implementowania przeciągania dotykowego, szczypania i obrotu
W środowiskach wielodotyku, takich jak te na urządzeniach przenośnych, użytkownicy często używają palców do manipulowania obiektami na ekranie. Typowe gesty, takie jak przeciąganie jednym palcem i szczypta dwu palcami, mogą przenosić i skalować obiekty, a nawet obracać je. Te gesty są zwykle implementowane przy użyciu macierzy transformacji, a w tym artykule pokazano, jak to zrobić.
Wszystkie pokazane tutaj przykłady używają efektu śledzenia dotykowego Xamarin.Forms przedstawionego w artykule Wywoływanie zdarzeń z efektów.
Przeciąganie i tłumaczenie
Jednym z najważniejszych zastosowań transformacji macierzy jest przetwarzanie dotykowe. Pojedyncza SKMatrix
wartość może skonsolidować serię operacji dotykowych.
W przypadku przeciągania SKMatrix
pojedynczego palca wartość wykonuje tłumaczenie. Jest to pokazane na stronie Przeciąganie mapy bitowej . Plik XAML tworzy wystąpienie elementu SKCanvasView
w pliku Xamarin.FormsGrid
. Obiekt TouchEffect
został dodany do Effects
kolekcji tego Grid
elementu :
<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>
Teoretycznie TouchEffect
obiekt można dodać bezpośrednio do Effects
kolekcji SKCanvasView
obiektu , ale nie działa na wszystkich platformach. Ponieważ parametr SKCanvasView
ma taki sam rozmiar jak Grid
w tej konfiguracji, dołączanie go do Grid
elementu działa tak samo dobrze.
Plik związany z kodem ładuje się w zasobie mapy bitowej w konstruktorze i wyświetla go w procedurze PaintSurface
obsługi:
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 żadnego dalszego kodu SKMatrix
wartość jest zawsze macierzą identyfikacji i nie będzie miała wpływu na wyświetlanie mapy bitowej. Celem OnTouchEffectAction
programu obsługi ustawionego w pliku XAML jest zmiana wartości macierzy w celu odzwierciedlenia manipulacji dotykiem.
Procedura OnTouchEffectAction
obsługi rozpoczyna się od przekonwertowania Xamarin.FormsPoint
wartości na wartość SkiaSharp SKPoint
. Jest to prosta kwestia skalowania na Width
podstawie właściwości SKCanvasView
i Height
(które są jednostkami niezależnymi od urządzenia) i CanvasSize
właściwością, która znajduje się w jednostkach pikseli:
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;
}
}
···
}
Gdy palec po raz pierwszy dotyka ekranu, zdarzenie typu TouchActionType.Pressed
jest uruchamiane. Pierwszym zadaniem jest ustalenie, czy palec dotyka mapy bitowej. Takie zadanie jest często nazywane testowaniem trafień. W takim przypadku można przeprowadzić testowanie trafień przez utworzenie SKRect
wartości odpowiadającej mapie bitowej, zastosowanie do niej MapRect
przekształcenia macierzy za pomocą elementu , a następnie określenie, czy punkt dotykowy znajduje się wewnątrz przekształconego prostokąta.
Jeśli tak jest, touchId
to pole jest ustawione na identyfikator dotykowy, a pozycja palca zostanie zapisana.
TouchActionType.Moved
W przypadku zdarzenia czynniki SKMatrix
tłumaczenia wartości są dostosowywane na podstawie bieżącej pozycji palca i nowej pozycji palca. Ta nowa pozycja zostanie zapisana po raz kolejny i SKCanvasView
unieważniona.
Podczas eksperymentu z tym programem zwróć uwagę, że możesz przeciągać mapę bitową tylko wtedy, gdy palec dotyka obszaru, w którym jest wyświetlana mapa bitowa. Chociaż to ograniczenie nie jest bardzo ważne dla tego programu, staje się kluczowe podczas manipulowania wieloma mapami bitowymi.
Szczypanie i skalowanie
Co chcesz zrobić, gdy dwa palce dotykają mapy bitowej? Jeśli dwa palce poruszają się równolegle, prawdopodobnie chcesz, aby mapa bitowa poruszała się wraz z palcami. Jeśli dwa palce wykonują operację szczyptowania lub rozciągania, możesz chcieć, aby mapa bitowa została obrócona (omówiono w następnej sekcji) lub przeskalowana. Podczas skalowania mapy bitowej najlepiej jest zachować dwa palce w tych samych pozycjach względem mapy bitowej i odpowiednio skalować mapę bitową.
Obsługa dwóch palców jednocześnie wydaje się skomplikowana, ale należy pamiętać, że TouchAction
program obsługi otrzymuje tylko informacje o jednym palcu naraz. Jeśli dwie palce manipulują mapą bitową, to dla każdego zdarzenia jeden palec zmienił położenie, ale drugi nie uległ zmianie. W poniższym kodzie strony Skalowanie map bitowych palec, który nie zmienił położenia, jest nazywany punktem przestawnym, ponieważ przekształcenie jest względem tego punktu.
Jedną z różnic między tym programem a poprzednim programem jest to, że należy zapisać wiele identyfikatorów dotykowych. Słownik jest używany w tym celu, gdzie touch ID jest kluczem słownika, a wartość słownika jest bieżącą pozycją tego palca:
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;
}
}
···
}
Obsługa Pressed
akcji jest prawie taka sama jak poprzedni program, z tą różnicą, że identyfikator i punkt dotykowy są dodawane do słownika. Akcje Released
i Cancelled
usuwają wpis słownika.
Obsługa Moved
akcji jest jednak bardziej skomplikowana. Jeśli jest tylko jeden palec, przetwarzanie jest bardzo takie samo jak w poprzednim programie. W przypadku co najmniej dwóch palców program musi również uzyskać informacje ze słownika obejmującego palec, który nie porusza się. Robi to przez skopiowanie kluczy słownika do tablicy, a następnie porównanie pierwszego klucza z identyfikatorem przenoszonego palca. Dzięki temu program może uzyskać punkt przestawny odpowiadający palcem, który nie porusza się.
Następnie program oblicza dwa wektory położenia nowego palca względem punktu przestawnego i starego położenia palca względem punktu przestawnego. Współczynniki tych wektorów są czynnikami skalowania. Ponieważ dzielenie według zera jest możliwe, muszą być sprawdzane pod kątem wartości nieskończonych lub wartości NaN (a nie liczby). Jeśli wszystko jest dobrze, przekształcenie skalowania jest łączone z wartością SKMatrix
zapisaną jako pole.
Podczas eksperymentu z tą stroną zauważysz, że możesz przeciągnąć mapę bitową z jednym lub dwoma palcami lub przeskalować ją dwoma palcami. Skalowanie jest anisotropowe, co oznacza, że skalowanie może być różne w kierunkach poziomych i pionowych. To zniekształca współczynnik proporcji, ale umożliwia również przerzucanie mapy bitowej w celu utworzenia obrazu dublowanego. Możesz również odkryć, że można zmniejszyć mapę bitową do wymiaru zerowego i zniknąć. W kodzie produkcyjnym warto chronić się przed tym.
Obrót dwoma palcami
Strona Obróć mapy bitowej umożliwia użycie dwóch palców do obrotu lub izotropowego skalowania. Mapa bitowa zawsze zachowuje prawidłowy współczynnik proporcji. Użycie dwóch palców do obrotu i anisotropowego skalowania nie działa bardzo dobrze, ponieważ ruch palców jest bardzo podobny dla obu zadań.
Pierwszą dużą różnicą w tym programie jest logika testowania trafień. Poprzednie programy używały Contains
metody SKRect
określania, czy punkt dotykowy znajduje się w przekształconym prostokątze odpowiadającym mapie bitowej. Jednak gdy użytkownik manipuluje mapą bitową, mapa bitowa może być obracana i SKRect
nie może prawidłowo reprezentować obróconego prostokąta. Można się obawiać, że logika testowania trafień musi implementować dość złożoną geometrię analityczną w tym przypadku.
Dostępny jest jednak skrót: Określenie, czy punkt znajduje się w granicach przekształconego prostokąta, jest taki sam jak określenie, czy odwrotny przekształcony punkt znajduje się w granicach nieprzetłumaczonego prostokąta. Jest to znacznie łatwiejsze obliczenie, a logika może nadal korzystać z wygodnej Contains
metody:
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 Moved
zdarzenia zaczyna się tak jak poprzedni program. Dwa wektory o nazwie oldVector
i newVector
są obliczane na podstawie poprzedniego i bieżącego punktu ruchomego palca oraz punktu obrotu palca unmoving. Jednak kąty tych wektorów są określane, a różnica jest kątem obrotu.
Skalowanie może być również zaangażowane, więc stary wektor jest obracany na podstawie kąta obrotu. Względna wielkość dwóch wektorów jest teraz czynnikiem skalowania. Zwróć uwagę, że ta sama scale
wartość jest używana do skalowania w poziomie i w pionie, aby skalowanie było izotropowe. Pole matrix
jest dostosowywane zarówno przez macierz obrotu, jak i macierz skalowania.
Jeśli aplikacja musi zaimplementować przetwarzanie dotykowe dla pojedynczej mapy bitowej (lub innego obiektu), możesz dostosować kod z tych trzech przykładów dla własnej aplikacji. Jeśli jednak musisz zaimplementować przetwarzanie dotykowe dla wielu map bitowych, prawdopodobnie chcesz hermetyzować te operacje dotykowe w innych klasach.
Hermetyzowanie operacji dotykowych
Strona Manipulowanie dotykiem pokazuje manipulowanie dotykiem pojedynczej mapy bitowej, ale przy użyciu kilku innych plików, które hermetyzują znaczną część logiki pokazanej powyżej. Pierwszy z tych plików to TouchManipulationMode
wyliczenie, które wskazuje różne typy manipulacji dotykiem zaimplementowane przez kod, który zobaczysz:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
to przeciąganie jednym palcem, które jest implementowane z tłumaczeniem. Wszystkie kolejne opcje obejmują również przesuwanie, ale obejmują dwa palce: IsotropicScale
to operacja szczypta, która powoduje równomierne skalowanie obiektu w kierunkach poziomych i pionowych. AnisotropicScale
umożliwia nierówne skalowanie.
Opcja ScaleRotate
dotyczy skalowania i obrotu dwu palcami. Skalowanie jest izotropiczne. Jak wspomniano wcześniej, implementowanie rotacji dwóch palców za pomocą skalowania anisotropowego jest problematyczne, ponieważ ruchy palców są zasadniczo takie same.
Opcja ScaleDualRotate
dodaje obrót jednym palcem. Gdy pojedynczy palec przeciąga obiekt, przeciągnięty obiekt jest najpierw obracany wokół środka, tak aby środek obiektu był wyrównany do wektora przeciągania.
Plik TouchManipulationPage.xaml zawiera element Picker
z elementami członkowskimi TouchManipulationMode
wyliczenia:
<?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>
W dolnej części znajduje się element i SKCanvasView
TouchEffect
dołączony do pojedynczej komórki Grid
, która ją otacza.
Plik TouchManipulationPage.xaml.cs bitmap
zawiera pole, ale nie jest typu .SKBitmap
Typ to TouchManipulationBitmap
(klasa, którą zobaczysz wkrótce):
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 tworzy wystąpienie TouchManipulationBitmap
obiektu, przekazując do konstruktora SKBitmap
obiekt uzyskany z zasobu osadzonego. Konstruktor kończy, ustawiając Mode
właściwość TouchManager
właściwości TouchManipulationBitmap
obiektu na element członkowski TouchManipulationMode
wyliczenia.
Program SelectedIndexChanged
obsługi dla Picker
tej właściwości ustawia również następującą Mode
właściwość:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
Procedura TouchAction
obsługi TouchEffect
wystąpienia w pliku XAML wywołuje dwie metody w TouchManipulationBitmap
nazwach HitTest
i 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;
}
}
...
}
HitTest
Jeśli metoda zwróci true
wartość — co oznacza, że palec dotknął ekranu w obszarze zajmowanym przez mapę bitową , identyfikator dotykowy zostanie dodany do TouchIds
kolekcji. Ten identyfikator reprezentuje sekwencję zdarzeń dotyku dla tego palca, aż palec unosi się z ekranu. Jeśli wiele palców dotyka mapy bitowej, touchIds
kolekcja zawiera identyfikator dotykowy dla każdego palca.
Procedura TouchAction
obsługi wywołuje również klasę ProcessTouchEvent
w pliku TouchManipulationBitmap
. W tym miejscu dochodzi do niektórych (ale nie wszystkich) rzeczywistego przetwarzania dotykowego.
Klasa TouchManipulationBitmap
jest klasą SKBitmap
otoki zawierającą kod renderowania mapy bitowej i przetwarzania zdarzeń dotykowych. Działa w połączeniu z bardziej uogólniony kod w TouchManipulationManager
klasie (co wkrótce zobaczysz).
Konstruktor TouchManipulationBitmap
zapisuje SKBitmap
i tworzy wystąpienie dwóch właściwości, TouchManager
właściwość typu TouchManipulationManager
i Matrix
właściwość 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; }
...
}
Ta Matrix
właściwość jest skumulowaną transformacją wynikającą ze wszystkich działań dotykowych. Jak zobaczysz, każde zdarzenie dotykowe jest rozpoznawane w macierzy, która jest następnie połączona z wartością SKMatrix
przechowywaną przez Matrix
właściwość.
Obiekt TouchManipulationBitmap
rysuje się w swojej Paint
metodzie. Argument jest obiektem SKCanvas
. Może to SKCanvas
już zostać zastosowane przekształcenie, więc Paint
metoda łączy Matrix
właściwość skojarzona z mapą bitową do istniejącej transformacji i przywraca kanwę po zakończeniu:
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
zwraca true
wartość, jeśli użytkownik dotyka ekranu w punkcie w granicach mapy bitowej. Używa to logiki pokazanej wcześniej na stronie Rotacja mapy bitowej:
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;
}
...
}
Druga publiczna metoda w pliku TouchManipulationBitmap
to ProcessTouchEvent
. Po wywołaniu tej metody została już ustalona, że zdarzenie dotykowe należy do tej konkretnej mapy bitowej. Metoda obsługuje słownik TouchManipulationInfo
obiektów, który jest po prostu poprzednim punktem i nowym punktem każdego palca:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Oto słownik i ProcessTouchEvent
sama 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;
}
}
...
}
W zdarzeniach Moved
i Released
metoda wywołuje metodę Manipulate
. W tym czasie obiekt touchDictionary
zawiera co najmniej jeden TouchManipulationInfo
obiekt. Jeśli element touchDictionary
zawiera jeden element, prawdopodobnie PreviousPoint
wartości i NewPoint
są nierówne i reprezentują ruch palca. Jeśli wiele palców dotyka mapy bitowej, słownik zawiera więcej niż jeden element, ale tylko jeden z tych elementów ma różne PreviousPoint
wartości i NewPoint
. Wszystkie pozostałe mają równe PreviousPoint
wartości i NewPoint
.
Jest to ważne: Manipulate
Metoda może zakładać, że przetwarza ruch tylko jednego palca. W momencie tego wywołania żaden z innych palców nie porusza się, a jeśli naprawdę porusza się (co jest prawdopodobne), te ruchy zostaną przetworzone w przyszłych wywołaniach do Manipulate
.
Metoda Manipulate
najpierw kopiuje słownik do tablicy dla wygody. Ignoruje wszystkie inne niż pierwsze dwa wpisy. Jeśli więcej niż dwa palce próbują manipulować mapą bitową, pozostałe są ignorowane. Manipulate
jest ostatnim elementem członkowskim programu 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;
}
}
Jeśli jeden palec manipuluje mapą bitową, Manipulate
wywołuje metodę OneFingerManipulate
TouchManipulationManager
obiektu . W przypadku dwóch palców wywołuje metodę TwoFingerManipulate
. Argumenty tych metod są takie same: prevPoint
argumenty i newPoint
reprezentują palec, który się porusza. pivotPoint
Ale argument jest inny dla dwóch wywołań:
W przypadku manipulowania pivotPoint
jednym palcem element jest środek mapy bitowej. Ma to na celu obracanie się jednym palcem. W przypadku manipulowania dwoma palcami zdarzenie wskazuje ruch tylko jednego palca, tak aby pivotPoint
był palcem, który nie porusza się.
W obu przypadkach TouchManipulationManager
zwraca SKMatrix
wartość, która metoda łączy się z bieżącą Matrix
właściwością TouchManipulationPage
używaną do renderowania mapy bitowej.
TouchManipulationManager
jest uogólniony i nie używa żadnych innych plików z wyjątkiem TouchManipulationMode
. Możesz użyć tej klasy bez konieczności wprowadzania zmian we własnych aplikacjach. Definiuje pojedynczą właściwość typu TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Prawdopodobnie jednak chcesz uniknąć AnisotropicScale
tej opcji. Dzięki tej opcji bardzo łatwo jest manipulować mapą bitową, aby jeden z czynników skalowania stał się zerowy. To sprawia, że mapa bitowa znika z oczu, nigdy nie wraca. Jeśli naprawdę potrzebujesz skalowania anisotropowego, warto ulepszyć logikę, aby uniknąć niepożądanych wyników.
TouchManipulationManager
używa wektorów, ale ponieważ w skiaSharp nie SKVector
ma żadnej struktury, SKPoint
zamiast tego jest używana. SKPoint
obsługuje operator odejmowania, a wynik może być traktowany jako wektor. Jedyną logiką specyficzną Magnitude
dla wektorów, która musi zostać dodana, jest obliczenie:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Za każdym razem, gdy zostanie wybrana rotacja, metody manipulowania jednym palcem i dwoma palcami najpierw obsługują rotację. Jeśli zostanie wykryta jakakolwiek rotacja, składnik rotacji zostanie skutecznie usunięty. To, co pozostaje, jest interpretowane jako przesuwanie i skalowanie.
Oto OneFingerManipulate
metoda. Jeśli obrót jednym palcem nie został włączony, logika jest prosta — po prostu używa poprzedniego punktu i nowego punktu do konstruowania wektora o nazwie delta
odpowiadającej dokładnie translacji. Po włączeniu obrotu jednym palcem metoda używa kątów z punktu przestawnego (środka mapy bitowej) do poprzedniego punktu i nowego punktu do utworzenia macierzy obrotu:
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;
}
...
}
W metodzie TwoFingerManipulate
punkt przestawny jest położeniem palca, który nie porusza się w tym konkretnym zdarzeniu dotykowym. Obrót jest bardzo podobny do obrotu jednym palcem, a następnie wektor o nazwie oldVector
(na podstawie poprzedniego punktu) jest dostosowywany do obrotu. Pozostały ruch jest interpretowany jako skalowanie:
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;
}
...
}
Zauważysz, że w tej metodzie nie ma jawnego tłumaczenia. Jednak zarówno MakeRotation
metody , jak i MakeScale
są oparte na punkcie przestawnym, i obejmuje niejawne tłumaczenie. Jeśli używasz dwóch palców na mapie bitowej i przeciągając je w tym samym kierunku, TouchManipulation
uzyskasz serię zdarzeń dotykowych naprzemiennych między dwoma palcami. Gdy każdy palec porusza się względem drugiego, skalowanie lub obracanie wyników, ale jest ono negowane przez ruch drugiego palca, a wynik jest tłumaczeniem.
Jedyną pozostałą częścią strony Manipulowanie dotykiem jest PaintSurface
procedura obsługi w TouchManipulationPage
pliku za pomocą kodu. Wywołuje to metodę Paint
obiektu TouchManipulationBitmap
, która stosuje macierz reprezentującą skumulowane działanie dotyku:
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));
}
}
Procedura PaintSurface
obsługi kończy się, wyświetlając MatrixDisplay
obiekt pokazujący skumulowaną macierz dotykową:
Manipulowanie wieloma mapami bitowymi
Jedną z zalet izolowania kodu przetwarzania dotykowego w klasach, takich jak TouchManipulationBitmap
i TouchManipulationManager
jest możliwość ponownego użycia tych klas w programie, który umożliwia użytkownikowi manipulowanie wieloma mapami bitowymi.
Na stronie Widok punktowy mapy bitowej pokazano, jak to zrobić. Zamiast definiować pole typu TouchManipulationBitmap
, BitmapScatterPage
klasa definiuje List
obiekty mapy bitowej:
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 ładuje wszystkie mapy bitowe dostępne jako zasoby osadzone i dodaje je do elementu bitmapCollection
. Zwróć uwagę, że Matrix
właściwość jest inicjowana na każdym TouchManipulationBitmap
obiekcie, więc lewe górne rogi każdej mapy bitowej są przesunięte o 100 pikseli.
Strona BitmapScatterView
musi również obsługiwać zdarzenia dotykowe dla wielu map bitowych. Zamiast definiować List
identyfikatory dotykowych aktualnie manipulowanych TouchManipulationBitmap
obiektów, ten program wymaga słownika:
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;
}
}
...
}
Zwróć uwagę, jak logika Pressed
przechodzi przez odwrotnie bitmapCollection
. Mapy bitowe często nakładają się na siebie. Mapy bitowe w dalszej części kolekcji wizualnie leżą na mapach bitowych wcześniej w kolekcji. Jeśli istnieje wiele map bitowych pod palcem, które naciskają na ekranie, najbardziej górny musi być ten, który jest manipulowany przez ten palec.
Zwróć również uwagę, że logika Pressed
przenosi mapę bitową na koniec kolekcji, aby wizualnie przenosiła się do góry stosu innych map bitowych.
W zdarzeniach Moved
i TouchAction
Released
program obsługi wywołuje metodę ProcessingTouchEvent
w TouchManipulationBitmap
sposób podobny do wcześniejszego programu.
PaintSurface
Na koniec program obsługi wywołuje metodę Paint
każdego TouchManipulationBitmap
obiektu:
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);
}
}
}
Kod przechodzi przez kolekcję i wyświetla stos map bitowych od początku kolekcji do końca:
Skalowanie pojedynczego palca
Operacja skalowania zwykle wymaga gestu szczypania przy użyciu dwóch palców. Można jednak zaimplementować skalowanie za pomocą pojedynczego palca, przesuwając rogi mapy bitowej palcem.
Jest to pokazane na stronie Skalowanie pojedynczego palca narożnego. Ponieważ w tym przykładzie jest używany nieco inny typ skalowania niż zaimplementowany w TouchManipulationManager
klasie, nie używa tej klasy ani TouchManipulationBitmap
klasy. Zamiast tego cała logika dotykowa znajduje się w pliku za pomocą kodu. Jest to nieco prostsza logika niż zwykle, ponieważ śledzi tylko jeden palec naraz, i po prostu ignoruje wszelkie pomocnicze palce, które mogą dotykać ekranu.
Strona SingleFingerCornerScale.xaml tworzy wystąpienie SKCanvasView
klasy i tworzy TouchEffect
obiekt do śledzenia zdarzeń dotykowych:
<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>
Plik SingleFingerCornerScalePage.xaml.cs ładuje zasób mapy bitowej z katalogu Media i wyświetla go przy użyciu obiektu zdefiniowanego SKMatrix
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);
}
···
}
Ten SKMatrix
obiekt jest modyfikowany przez logikę dotykową pokazaną poniżej.
Pozostała część pliku kodu jest procedurą TouchEffect
obsługi zdarzeń. Zaczyna się od przekonwertowania bieżącej lokalizacji palca na SKPoint
wartość. Pressed
W przypadku typu akcji program obsługi sprawdza, czy żaden inny palec nie dotyka ekranu i że palec znajduje się w granicach mapy bitowej.
Kluczową częścią kodu jest instrukcja obejmująca if
dwa wywołania Math.Pow
metody . Ta matematyka sprawdza, czy lokalizacja palca znajduje się poza wielokropkiem, który wypełnia mapę bitową. Jeśli tak, jest to operacja skalowania. Palec znajduje się w pobliżu jednego z narożników mapy bitowej, a punkt obrotu określa się, że jest to przeciwny róg. Jeśli palec znajduje się w tym wielokropce, jest to regularna operacja przesuwania:
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;
}
}
}
Moved
Typ akcji oblicza macierz odpowiadającą aktywności dotykowej od czasu naciśnięcia palca ekranu do tej pory. Łączy macierz z macierzą w efekcie w momencie, gdy palec po raz pierwszy nacisnął mapę bitową. Operacja skalowania jest zawsze względna względem rogu przeciwnego do tego, którego dotknął palec.
W przypadku małych lub podłużnych map bitowych wewnętrzny wielokropek może zajmować większość mapy bitowej i pozostawić małe obszary na rogach, aby skalować mapę bitową. Możesz wolisz nieco inne podejście, w takim przypadku możesz zastąpić cały if
blok ustawiany isScaling
na true
następujący kod:
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);
}
}
Ten kod skutecznie dzieli obszar mapy bitowej na wewnętrzny kształt rombu i cztery trójkąty w rogach. Pozwala to na chwytanie i skalowanie mapy bitowej znacznie większych obszarów w rogach.