觸控操作
使用矩陣轉換來實作觸控拖曳、捏合和旋轉
在行動裝置上的多觸控環境中,使用者通常會使用手指來操作螢幕上的物件。 常見的手勢,例如單指拖曳和雙指捏合可以移動和縮放物件,甚至旋轉它們。 這些手勢通常會使用轉換矩陣來實作,本文說明如何執行此動作。
此處顯示的所有範例都會使用 Xamarin.Forms 叫用效果中的事件一文 中呈現的觸控追蹤效果。
拖曳和翻譯
矩陣轉換最重要的應用程式之一是觸控處理。 單 SKMatrix
一值可以合併一系列的觸控作業。
針對單指拖曳, SKMatrix
值會執行翻譯。 這會在 [點陣圖拖曳 ] 頁面中示範。 XAML 檔案會在 中Xamarin.FormsGrid
具現化 SKCanvasView
。 TouchEffect
物件已加入至Effects
該 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>
理論上, TouchEffect
物件可以直接新增至 Effects
的 SKCanvasView
集合,但無法在所有平台上運作。 SKCanvasView
由於的大小與Grid
此組態中的 相同,因此將它附加至 Grid
的運作方式也一樣。
程式代碼後置檔案會在其建構函式中的點陣圖資源中載入,並在處理程式中 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());
}
}
如果沒有任何進一步的程式代碼,值 SKMatrix
一律是識別矩陣,而且它不會影響位圖的顯示。 XAML 檔案中設定的 OnTouchEffectAction
處理程式目標是改變矩陣值以反映觸控操作。
處理程式 OnTouchEffectAction
會從將值轉換成 Xamarin.FormsPoint
SkiaSharp SKPoint
值開始。 這是根據 Width
的 和 Height
屬性 SKCanvasView
調整的簡單事項(這些是裝置獨立單位)和 CanvasSize
屬性,其單位為圖元:
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;
}
}
···
}
當手指第一次觸碰螢幕時,會引發 類型的 TouchActionType.Pressed
事件。 第一個工作是判斷手指是否觸碰位圖。 這類工作通常稱為 點擊測試。 在此情況下,點擊測試可以藉由建立 SKRect
對應至位圖的值、使用 MapRect
套用矩陣轉換到位圖,然後判斷觸控點是否位於轉換的矩形內來完成。
如果是這種情況,則 touchId
欄位會設定為觸控標識碼,並儲存手指位置。
TouchActionType.Moved
針對 事件,值的轉譯因數SKMatrix
會根據手指的目前位置和手指的新位置進行調整。 該新位置會在下次通過 時儲存,且 SKCanvasView
已失效。
當您實驗此程式時,請注意,當您的手指觸碰到顯示點陣圖的區域時,您只能拖曳位圖。 雖然這項限制對這個程式並不十分重要,但在操作多個點陣圖時會變得很重要。
捏合和調整
當兩根手指觸碰點陣圖時,您要怎麼做? 如果兩個手指平行移動,則您可能希望點圖與手指一起移動。 如果兩根手指執行捏合或延展作業,則您可能會想要旋轉位圖(在下一節中討論)或縮放。 調整點陣圖時,兩根手指在相對於點陣圖的相同位置,以及據以縮放點陣圖時,最合理。
一次處理兩根手指似乎很複雜,但請記住, TouchAction
處理程式一次只接收一根手指的相關信息。 如果兩根手指正在操作位圖,則針對每個事件,一根手指已變更位置,但另一根手指並未變更。 在下方的 點陣圖縮放 頁面代碼中,未變更位置的手指稱為 樞紐 點,因為轉換相對於該點。
此程式與上一個程式之間的差異在於必須儲存多個觸控標識碼。 字典用於此目的,其中觸控標識符是字典索引鍵,而字典值是該手指的目前位置:
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;
}
}
···
}
動作的 Pressed
處理幾乎與上一個程式相同,不同之處在於標識符和觸控點會新增至字典。 Released
和 Cancelled
動作會移除字典專案。
不過,動作的 Moved
處理更為複雜。 如果只牽涉到一根手指,則處理與上一個程式非常相同。 針對兩個或多個手指,程式也必須從涉及未移動手指的字典中取得資訊。 其方式是將字典索引鍵複製到陣列,然後比較第一個索引鍵與移動的手指標識碼。 這可讓程式取得對應至未移動之手指的樞紐點。
接下來,程式會計算新手指位置相對於樞紐點的兩個向量,以及相對於樞紐點的舊手指位置。 這些向量的比例是縮放比例。 由於除以零是可能的,因此必須檢查無限值或 NaN(而非數位)值。 如果一切順利,縮放轉換就會與儲存為欄位的值串連 SKMatrix
。
當您實驗此頁面時,您會發現您可以使用一或兩根手指來拖曳點陣圖,或使用兩根手指來縮放位圖。 縮放比例為 非等性,這表示縮放比例在水平和垂直方向上可能不同。 這會扭曲外觀比例,但也可讓您翻轉位圖來製作鏡像影像。 您也可以發現您可以將點陣圖壓縮為零維度,而且它消失。 在實際執行程式代碼中,您會想要防範這種情況。
雙指旋轉
[ 點陣圖旋轉 ] 頁面可讓您使用兩根手指進行旋轉或等向縮放。 位圖一律會保留其正確的外觀比例。 使用兩根手指進行旋轉和異向性縮放效果不佳,因為兩個工作的手指移動非常類似。
此程式的第一大差異是點擊測試邏輯。 先前的程式會使用 Contains
的 方法來 SKRect
判斷觸控點是否位於對應至位圖的轉換矩形內。 但是,當使用者操作位圖時,點陣圖可能會旋轉,而且 SKRect
無法正確表示旋轉的矩形。 您可能會擔心點擊測試邏輯需要在該案例中實作相當複雜的分析幾何。
不過,有可用的快捷方式:判斷某個點是否位於已轉換矩形的界限內,與判斷反向轉換點是否位於未轉換矩形的界限內相同。 這是一個更容易的計算,邏輯可以繼續使用方便 Contains
的方法:
public partial class BitmapRotationPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (!touchDictionary.ContainsKey(args.Id))
{
// Invert the matrix
if (matrix.TryInvert(out SKMatrix inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(point);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
if (rect.Contains(transformedPoint))
{
touchDictionary.Add(args.Id, point);
}
}
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger rotate, scale, and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Isotropic scaling!
float scale = Magnitude(newVector) / Magnitude(oldVector);
if (!float.IsNaN(scale) && !float.IsInfinity(scale))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));
SKMatrix.PostConcat(ref matrix, touchMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
···
}
事件的邏輯 Moved
會像上一個程序一樣開始。 名為 和 newVector
的oldVector
兩個向量是根據移動手指的上一個和目前點和移動手指的樞紐點來計算。 但接著會決定這些向量的角度,而差異則是旋轉角度。
也可能涉及縮放比例,因此舊向量會根據旋轉角度旋轉。 兩個向量的相對大小現在是縮放比例。 請注意,水平 scale
和垂直縮放使用相同的值,讓縮放比例為等向性。 欄位 matrix
會由旋轉矩陣和尺規矩陣調整。
如果您的應用程式需要實作單一位圖的觸控處理(或其他物件),您可以針對您自己的應用程式,從這三個範例調整程序代碼。 但是,如果您需要為多個點陣圖實作觸控處理,您可能想要在其他類別中封裝這些觸控作業。
封裝觸控作業
[ 觸控操作 ] 頁面示範單一位圖的觸控操作,但使用數個其他檔案來封裝上述大部分邏輯。 這些檔案的第一個是 TouchManipulationMode
列舉,指出您將看到的程式代碼所實作的不同觸控操作類型:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
是使用翻譯實作的單指拖曳。 所有後續選項也都包含移動流覽,但牽涉到兩根手指: IsotropicScale
是一種捏合作業,導致對象在水準和垂直方向上同樣縮放。 AnisotropicScale
允許不相等的調整。
此選項 ScaleRotate
適用於雙指縮放和旋轉。 縮放比例為等向性。 如先前所述,使用異向性縮放實作雙指旋轉是有問題的,因為手指移動基本上相同。
選項 ScaleDualRotate
會新增單指旋轉。 當單指拖曳物件時,拖曳的物件會先繞其中心旋轉,讓物件中央與拖曳向量對齊。
TouchManipulationPage.xaml 檔案包含 Picker
具有 列舉成員的 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>
朝底部是 , SKCanvasView
並附加至將它括住的單一 TouchEffect
單元格 Grid
。
TouchManipulationPage.xaml.cs程式代碼後置檔案具有 bitmap
字段,但不是類型SKBitmap
。 這個類型為 TouchManipulationBitmap
(您很快就會看到的類別):
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;
}
}
...
}
建構函式會具現化 TouchManipulationBitmap
對象,傳遞至從內嵌資源取得的建構函 SKBitmap
式。 建構函式會藉由將 物件的 屬性的 屬性TouchManager
TouchManipulationBitmap
設定Mode
為 列舉的成員TouchManipulationMode
來結束。
的 SelectedIndexChanged
處理程式 Picker
也會設定這個 Mode
屬性:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
TouchAction
在 XAML 檔案中具現化的 處理程式TouchEffect
會呼叫 名為 HitTest
和 ProcessTouchEvent
中的TouchManipulationBitmap
兩個方法:
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
如果方法傳true
回 ,表示手指已觸碰位圖所佔用區域內的螢幕,則會將觸控標識元新增至TouchIds
集合。 此標識子代表該手指的觸控事件序列,直到手指從螢幕抬起為止。 如果多個手指觸碰位圖,則 touchIds
集合會包含每個手指的觸控標識碼。
處理程式TouchAction
也會在中TouchManipulationBitmap
呼叫 ProcessTouchEvent
類別。 這是實際觸控處理的一些(但並非全部)發生的地方。
類別 TouchManipulationBitmap
是 的 SKBitmap
包裝函式類別,其中包含用來轉譯位圖和處理觸控事件的程序代碼。 它可與類別中 TouchManipulationManager
更一般化的程式代碼搭配運作(您很快就會看到)。
建 TouchManipulationBitmap
構函式會 SKBitmap
儲存 並具現化兩個屬性: TouchManager
類型的 TouchManipulationManager
屬性和 Matrix
類型的 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; }
...
}
這個 Matrix
屬性是所有觸控活動所產生的累積轉換。 如您所見,每個觸控事件都會解析成矩陣,然後與 屬性所Matrix
儲存的值串連SKMatrix
。
物件 TouchManipulationBitmap
會在其 Paint
方法中繪製本身。 自變數是 SKCanvas
物件。 這可能 SKCanvas
已經套用轉換,因此 Paint
方法會將與位圖相關聯的屬性串 Matrix
連至現有的轉換,並在完成時還原畫布:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
如果使用者在點觸碰螢幕,此方法 HitTest
會傳回 true
。 這會使用稍早在 [點陣圖旋轉 ] 頁面中顯示的邏輯:
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;
}
...
}
中的TouchManipulationBitmap
ProcessTouchEvent
第二個公用方法是 。 呼叫此方法時,已經建立觸控事件屬於這個特定位圖。 方法會維護 物件的字典 TouchManipulationInfo
,其只是前一個點和每個手指的新點:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
以下是字典和 ProcessTouchEvent
方法本身:
class TouchManipulationBitmap
{
...
Dictionary<long, TouchManipulationInfo> touchDictionary =
new Dictionary<long, TouchManipulationInfo>();
...
public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
{
switch (type)
{
case TouchActionType.Pressed:
touchDictionary.Add(id, new TouchManipulationInfo
{
PreviousPoint = location,
NewPoint = location
});
break;
case TouchActionType.Moved:
TouchManipulationInfo info = touchDictionary[id];
info.NewPoint = location;
Manipulate();
info.PreviousPoint = info.NewPoint;
break;
case TouchActionType.Released:
touchDictionary[id].NewPoint = location;
Manipulate();
touchDictionary.Remove(id);
break;
case TouchActionType.Cancelled:
touchDictionary.Remove(id);
break;
}
}
...
}
在 Moved
與 Released
事件中,方法會呼叫 Manipulate
。 這些時候, touchDictionary
包含一或多個 TouchManipulationInfo
物件。 touchDictionary
如果 包含一個專案,則和 NewPoint
值可能PreviousPoint
不相等,並代表手指的移動。 如果多指觸碰位圖,字典會包含多個專案,但其中只有一個專案具有不同的 PreviousPoint
和 NewPoint
值。 其餘所有都有相等 PreviousPoint
和 NewPoint
值。
這很重要:方法 Manipulate
可以假設它只會處理一根手指的移動。 在這個呼叫時,沒有其他手指移動,如果他們真的移動(可能),這些移動將在未來的呼叫 Manipulate
中處理。
Manipulate
方法會先將字典複製到陣列,以方便起見。 它會忽略前兩個專案以外的任何專案。 如果兩個以上的手指嘗試操作位圖,則會忽略其他手指。 Manipulate
是 的最後一個成員 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;
}
}
如果一指正在操作位圖,請 Manipulate
呼叫 OneFingerManipulate
物件的方法 TouchManipulationManager
。 針對兩根手指,它會呼叫 TwoFingerManipulate
。 這些方法的自變數相同: prevPoint
和 newPoint
自變數代表移動的手指。 但兩個呼叫的 pivotPoint
自變數不同:
若為單指操作, pivotPoint
則為位圖的中心。 這是允許單指旋轉。 如果是雙指操作,事件表示只有一根手指的移動,因此 pivotPoint
是未移動的手指。
在這兩種情況下SKMatrix
,TouchManipulationManager
都會傳回 值,此方法會與用來呈現位圖的目前Matrix
屬性TouchManipulationPage
串連。
TouchManipulationManager
已一般化,而且除了 之外 TouchManipulationMode
,不會使用其他任何檔案。 您可能可以在自己的應用程式中使用這個類別,而不需要變更。 它會定義一個 TouchManipulationMode
類型的單一屬性:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
不過,您可能想要避免 AnisotropicScale
選項。 這個選項很容易操作位圖,讓其中一個縮放因數變成零。 這使得位圖從視線消失,永遠不會傳回。 如果您確實需要非等性調整,您會想要增強邏輯,以避免不想要的結果。
TouchManipulationManager
會使用向量,但因為SkiaSharp 中沒有 SKVector
結構, SKPoint
因此會改用 。 SKPoint
支援減法運算符,而且結果可以視為向量。 唯一 Magnitude
需要加入的向量特定邏輯是計算:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
每當選取旋轉時,單指和雙指操作方法都會先處理旋轉。 如果偵測到任何旋轉,則會有效地移除旋轉元件。 剩餘的內容會解譯為移動瀏覽和調整。
以下是 OneFingerManipulate
方法。 如果未啟用單指旋轉,則邏輯很簡單,它只會使用先前的點和新點來建構對應至翻譯的 delta
向量。 啟用單指旋轉時,方法會使用從樞紐點(點陣陣)到上一個點和新點的角度來建構旋轉矩陣:
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
,樞紐點是手指的位置,不會在這個特定的觸控事件中移動。 旋轉非常類似於單指旋轉,然後針對旋轉調整名為 oldVector
的向量(根據上一個點)。 剩餘的移動會解譯為縮放比例:
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;
}
...
}
您會發現此方法中沒有明確的翻譯。 不過, MakeRotation
和 MakeScale
方法都是以樞紐點為基礎,且包含隱含轉譯。 如果您在點陣圖上使用兩根手指,並以相同的方向拖曳它們, TouchManipulation
將會取得兩根手指之間交替的一系列觸控事件。 當每個手指相對於另一個手指移動時,縮放或旋轉結果,但它被另一根手指的移動所否定,而結果是翻譯。
觸控操作頁面的唯一PaintSurface
剩餘部分是程式代碼後置檔案中的TouchManipulationPage
處理程式。 這會呼叫 Paint
的 TouchManipulationBitmap
方法,這個方法會套用表示累積觸控活動的矩陣:
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));
}
}
處理程式會 PaintSurface
藉由顯示 MatrixDisplay
累積觸控矩陣的 對象來結束:
操作多個點陣圖
在這類 TouchManipulationBitmap
類別中隔離觸控處理程式碼的優點之一,就是 TouchManipulationManager
能夠在程式中重複使用這些類別,讓使用者操作多個點圖。
[ 點陣圖散佈圖檢視 ] 頁面示範如何完成此作業。 類別會定義List
點陣圖物件的,而不是定義型BitmapScatterPage
TouchManipulationBitmap
別的欄位:
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;
}
}
}
}
...
}
建構函式會載入所有可用為內嵌資源的點陣圖,並將其新增至 bitmapCollection
。 請注意, Matrix
屬性會在每個 TouchManipulationBitmap
物件上初始化,因此每個點陣圖的左上角會位移 100 圖元。
頁面 BitmapScatterView
也需要處理多個點陣圖的觸控事件。 此程式不需要定義 List
目前操作 TouchManipulationBitmap
物件的觸控識別碼,而是需要字典:
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;
}
}
...
}
請注意邏輯如何 Pressed
反向迴圈 bitmapCollection
。 位圖通常會彼此重疊。 集合稍後的點陣圖會以視覺方式躺在集合稍早的點陣圖之上。 如果手指下有多個點陣圖按下螢幕上,最上層的點陣圖必須是該手指操作的最上層位圖。
另請注意,邏輯會將 Pressed
該位圖移至集合結尾,以便以視覺方式移至其他點陣圖堆的頂端。
在 Moved
和 事件中TouchAction
,處理程式會像先前的程序一樣呼叫 ProcessingTouchEvent
方法TouchManipulationBitmap
Released
。
最後,處理程式會 PaintSurface
呼叫 Paint
每個 TouchManipulationBitmap
物件的 方法:
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);
}
}
}
程式代碼會迴圈查看集合,並顯示集合開頭到結尾的點陣圖堆:
單指縮放
調整作業通常需要使用兩根手指的捏合手勢。 不過,藉由手指移動位圖的角落,即可使用單指實作縮放。
這會在 [單指角刻度 ] 頁面中示範。 由於此範例使用的縮放類型與 類別中 TouchManipulationManager
實作的類型稍有不同,所以不會使用該類別或 TouchManipulationBitmap
類別。 相反地,所有的觸控邏輯都在程序代碼後置檔案中。 這比平常更簡單的邏輯,因為它一次只追蹤一根手指,而且只會忽略任何可能觸碰螢幕的次要手指。
SingleFingerCornerScale.xaml 頁面會具現化 類別,SKCanvasView
並建立TouchEffect
對象來追蹤觸控事件:
<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>
SingleFingerCornerScalePage.xaml.cs檔案會從 Media 目錄載入點陣圖資源,並使用SKMatrix
定義為欄位的物件加以顯示:
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);
}
···
}
此 SKMatrix
物件是由如下所示的觸控邏輯所修改。
程序代碼後置檔案的其餘部分是 TouchEffect
事件處理程式。 其開頭是將手指的目前位置轉換成 SKPoint
值。 Pressed
針對動作類型,處理程式會檢查沒有其他手指觸碰螢幕,而且手指位於位圖的界限內。
程式代碼的關鍵部分是一個語句,涉及對 方法的兩個 if
呼叫 Math.Pow
。 這個數學運算會檢查手指位置是否位於填滿位圖的橢圓形之外。 如果是,則這是調整作業。 手指靠近位圖的其中一個角落,而樞紐點則判斷為相反的角落。 如果手指在此橢圓形內,則為一般移動瀏覽作業:
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
類型會計算對應到觸控活動的矩陣,從手指按下螢幕到這次為止。 它會串連矩陣與矩陣在手指第一次按下位圖時生效。 縮放作業一律與手指觸碰的角落相反。
對於小型或長方形位圖,內部橢圓形可能會佔用大部分點陣圖,並將小區域留在角落以縮放位圖。 您可能偏好使用一些不同的方法,在此情況下,您可以使用下列程式代碼取代設定isScaling
為 true
的整個if
區塊:
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);
}
}
此程式代碼會有效地將點陣圖的區域分割成內部菱形和角落的四個三角形。 這可讓角落的較大區域抓取和縮放位圖。