共用方式為


觸控操作

使用矩陣轉換來實作觸控拖曳、捏合和旋轉

在行動裝置上的多觸控環境中,使用者通常會使用手指來操作螢幕上的物件。 常見的手勢,例如單指拖曳和雙指捏合可以移動和縮放物件,甚至旋轉它們。 這些手勢通常會使用轉換矩陣來實作,本文說明如何執行此動作。

受轉譯、縮放和旋轉約束的點陣圖

此處顯示的所有範例都會使用 Xamarin.Forms 叫用效果中的事件一文 中呈現的觸控追蹤效果

拖曳和翻譯

矩陣轉換最重要的應用程式之一是觸控處理。 單 SKMatrix 一值可以合併一系列的觸控作業。

針對單指拖曳, SKMatrix 值會執行翻譯。 這會在 [點陣圖拖曳 ] 頁面中示範。 XAML 檔案會在 中Xamarin.FormsGrid具現化 SKCanvasViewTouchEffect物件已加入至EffectsGrid的集合:

<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 物件可以直接新增至 EffectsSKCanvasView集合,但無法在所有平台上運作。 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 處理幾乎與上一個程式相同,不同之處在於標識符和觸控點會新增至字典。 ReleasedCancelled 動作會移除字典專案。

不過,動作的 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 會像上一個程序一樣開始。 名為 和 newVectoroldVector兩個向量是根據移動手指的上一個和目前點和移動手指的樞紐點來計算。 但接著會決定這些向量的角度,而差異則是旋轉角度。

也可能涉及縮放比例,因此舊向量會根據旋轉角度旋轉。 兩個向量的相對大小現在是縮放比例。 請注意,水平 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 式。 建構函式會藉由將 物件的 屬性的 屬性TouchManagerTouchManipulationBitmap設定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會呼叫 名為 HitTestProcessTouchEvent中的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;
    }
    ...
}

中的TouchManipulationBitmapProcessTouchEvent第二個公用方法是 。 呼叫此方法時,已經建立觸控事件屬於這個特定位圖。 方法會維護 物件的字典 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;
        }
    }
    ...
}

MovedReleased 事件中,方法會呼叫 Manipulate。 這些時候, touchDictionary 包含一或多個 TouchManipulationInfo 物件。 touchDictionary如果 包含一個專案,則和 NewPoint 值可能PreviousPoint不相等,並代表手指的移動。 如果多指觸碰位圖,字典會包含多個專案,但其中只有一個專案具有不同的 PreviousPointNewPoint 值。 其餘所有都有相等 PreviousPointNewPoint 值。

這很重要:方法 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。 這些方法的自變數相同: prevPointnewPoint 自變數代表移動的手指。 但兩個呼叫的 pivotPoint 自變數不同:

若為單指操作, pivotPoint 則為位圖的中心。 這是允許單指旋轉。 如果是雙指操作,事件表示只有一根手指的移動,因此 pivotPoint 是未移動的手指。

在這兩種情況下SKMatrixTouchManipulationManager都會傳回 值,此方法會與用來呈現位圖的目前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;
    }
    ...
}

您會發現此方法中沒有明確的翻譯。 不過, MakeRotationMakeScale 方法都是以樞紐點為基礎,且包含隱含轉譯。 如果您在點陣圖上使用兩根手指,並以相同的方向拖曳它們, TouchManipulation 將會取得兩根手指之間交替的一系列觸控事件。 當每個手指相對於另一個手指移動時,縮放或旋轉結果,但它被另一根手指的移動所否定,而結果是翻譯。

觸控操作頁面的唯一PaintSurface剩餘部分是程式代碼後置檔案中的TouchManipulationPage處理程式。 這會呼叫 PaintTouchManipulationBitmap方法,這個方法會套用表示累積觸控活動的矩陣:

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點陣圖物件的,而不是定義型BitmapScatterPageTouchManipulationBitmap別的欄位:

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 方法TouchManipulationBitmapReleased

最後,處理程式會 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 類型會計算對應到觸控活動的矩陣,從手指按下螢幕到這次為止。 它會串連矩陣與矩陣在手指第一次按下位圖時生效。 縮放作業一律與手指觸碰的角落相反。

對於小型或長方形位圖,內部橢圓形可能會佔用大部分點陣圖,並將小區域留在角落以縮放位圖。 您可能偏好使用一些不同的方法,在此情況下,您可以使用下列程式代碼取代設定isScalingtrue 的整個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);
    }
}

此程式代碼會有效地將點陣圖的區域分割成內部菱形和角落的四個三角形。 這可讓角落的較大區域抓取和縮放位圖。