Sdílet prostřednictvím


Manipulace dotyků

Použití maticových transformací k implementaci dotykového přetahování, připnutí a otočení

V prostředích s více dotykovými ovládáními, jako jsou například na mobilních zařízeních, uživatelé často používají prsty k manipulaci s objekty na obrazovce. Běžná gesta, jako je přetažení jedním prstem a dvoupístný špendlík, můžou přesouvat a škálovat objekty nebo je dokonce otáčet. Tato gesta se obecně implementují pomocí transformovaných matic a v tomto článku se dozvíte, jak to udělat.

Rastrový obrázek, na který se vztahuje překlad, změna měřítka a otočení

Všechny zde uvedené ukázky používají efekt dotykového Xamarin.Forms sledování uvedený v článku Vyvolání událostí z efektů.

Přetahování a překlad

Jedním z nejdůležitějších aplikací maticových transformací je dotykové zpracování. Jedna SKMatrix hodnota může konsolidovat řadu dotykových operací.

Při přetahování jedním prstem provede SKMatrix hodnota překlad. To je znázorněno na stránce Přetažení bitmapy . Soubor XAML vytvoří SKCanvasView instanci v objektu Xamarin.FormsGrid. Objekt TouchEffect byl přidán do Effects kolekce, která Grid:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Transforms.BitmapDraggingPage"
             Title="Bitmap Dragging">
    
    <Grid BackgroundColor="White">
        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction" />
        </Grid.Effects>
    </Grid>
</ContentPage>

Teoreticky TouchEffect by objekt mohl být přidán přímo do Effects kolekce SKCanvasView, ale to nefunguje na všech platformách. Vzhledem k tomu, že je stejná SKCanvasView velikost jako Grid v této konfiguraci, připojte ji k Grid práci stejně dobře.

Soubor s kódem se načte v bitmapovém prostředku v jeho konstruktoru a zobrazí ho v obslužné rutině PaintSurface :

public partial class BitmapDraggingPage : ContentPage
{
    // Bitmap and matrix for display
    SKBitmap bitmap;
    SKMatrix matrix = SKMatrix.MakeIdentity();
    ···

    public BitmapDraggingPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, new SKPoint());
    }
}

Bez dalšího kódu SKMatrix je hodnota vždy identifikovat matici a nemělo by žádný vliv na zobrazení rastrového obrázku. Cílem obslužné OnTouchEffectAction rutiny nastavené v souboru XAML je změnit maticovou hodnotu tak, aby odrážela manipulace s dotykem.

Obslužná rutina OnTouchEffectAction začíná převodem Xamarin.FormsPoint hodnoty na hodnotu SkiaSharp SKPoint . Jedná se o jednoduchou záležitost škálování na základě vlastností SKCanvasView Height (které jsou jednotky nezávislé na Width zařízení) a CanvasSize vlastnosti, které jsou v jednotkách pixelů:

public partial class BitmapDraggingPage : ContentPage
{
    ···
    // Touch information
    long touchId = -1;
    SKPoint previousPoint;
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point = 
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Find transformed bitmap rectangle
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = matrix.MapRect(rect);

                // Determine if the touch was within that rectangle
                if (rect.Contains(point))
                {
                    touchId = args.Id;
                    previousPoint = point;
                }
                break;

            case TouchActionType.Moved:
                if (touchId == args.Id)
                {
                    // Adjust the matrix for the new position
                    matrix.TransX += point.X - previousPoint.X;
                    matrix.TransY += point.Y - previousPoint.Y;
                    previousPoint = point;
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = -1;
                break;
        }
    }
    ···
}

Když se prst poprvé dotkne obrazovky, aktivuje se událost typu TouchActionType.Pressed . Prvním úkolem je určit, zda se prst dotkne rastrového obrázku. Takový úkol se často nazývá hit-testing. V tomto případě lze dosáhnout hit-testování vytvořením SKRect hodnoty odpovídající rastrový obrázek, použití maticové transformace s MapRecta pak určit, zda je dotykový bod uvnitř transformovaného obdélníku.

V takovém případě touchId je pole nastavené na touch ID a umístění prstu se uloží.

TouchActionType.Moved U události jsou faktory překladu SKMatrix hodnoty upraveny na základě aktuální pozice prstu a nové pozice prstu. Tato nová pozice se uloží až příště a SKCanvasView zneplatní se.

Při experimentování s tímto programem mějte na paměti, že rastrový obrázek můžete přetáhnout pouze tehdy, když se prst dotkne oblasti, ve které je rastrový obrázek zobrazen. I když toto omezení není pro tento program velmi důležité, stává se zásadní při manipulaci s více rastrovými obrázky.

Připnutí a škálování

Co se má stát, když se dva prsty dotkne rastrového obrázku? Pokud se dva prsty pohybují paralelně, pravděpodobně chcete, aby se rastrový obrázek posouvat spolu s prsty. Pokud dva prsty provádějí operaci stažení nebo roztažení, můžete chtít, aby se rastrový obrázek otočil (aby se probíral v další části) nebo škáloval. Při škálování rastrového obrázku dává smysl, aby dva prsty zůstaly ve stejných pozicích vzhledem k bitmapě a aby se rastrový obrázek odpovídajícím způsobem škáloval.

Zpracování dvou prstů najednou vypadá složitě, ale mějte na paměti, že TouchAction obslužná rutina přijímá informace pouze o jednom prstu najednou. Pokud dva prsty manipulují s rastrem, pak u každé události došlo ke změně pozice jednoho prstu, ale druhý se nezměnil. V rastrovém kódu stránky měřítko níže se prst, který nezměnil pozici, nazývá kontingenční bod, protože transformace je relativní k danému bodu.

Jedním z rozdílů mezi tímto programem a předchozím programem je, že je nutné uložit více dotykových ID. Slovník se používá pro tento účel, kde touch ID je klíč slovníku a hodnota slovníku je aktuální pozice tohoto prstu:

public partial class BitmapScalingPage : ContentPage
{
    ···
    // Touch information
    Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Find transformed bitmap rectangle
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = matrix.MapRect(rect);

                // Determine if the touch was within that rectangle
                if (rect.Contains(point) && !touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Add(args.Id, point);
                }
                break;

            case TouchActionType.Moved:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    // Single-finger drag
                    if (touchDictionary.Count == 1)
                    {
                        SKPoint prevPoint = touchDictionary[args.Id];

                        // Adjust the matrix for the new position
                        matrix.TransX += point.X - prevPoint.X;
                        matrix.TransY += point.Y - prevPoint.Y;
                        canvasView.InvalidateSurface();
                    }
                    // Double-finger scale and drag
                    else if (touchDictionary.Count >= 2)
                    {
                        // Copy two dictionary keys into array
                        long[] keys = new long[touchDictionary.Count];
                        touchDictionary.Keys.CopyTo(keys, 0);

                        // Find index of non-moving (pivot) finger
                        int pivotIndex = (keys[0] == args.Id) ? 1 : 0;

                        // Get the three points involved in the transform
                        SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                        SKPoint prevPoint = touchDictionary[args.Id];
                        SKPoint newPoint = point;

                        // Calculate two vectors
                        SKPoint oldVector = prevPoint - pivotPoint;
                        SKPoint newVector = newPoint - pivotPoint;

                        // Scaling factors are ratios of those
                        float scaleX = newVector.X / oldVector.X;
                        float scaleY = newVector.Y / oldVector.Y;

                        if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
                            !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
                        {
                            // If something bad hasn't happened, calculate a scale and translation matrix
                            SKMatrix scaleMatrix = 
                                SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);

                            SKMatrix.PostConcat(ref matrix, scaleMatrix);
                            canvasView.InvalidateSurface();
                        }
                    }

                    // Store the new point in the dictionary
                    touchDictionary[args.Id] = point;
                }

                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Remove(args.Id);
                }
                break;
        }
    }
    ···
}

Zpracování Pressed akce je téměř stejné jako předchozí program s tím rozdílem, že ID a dotykový bod jsou přidány do slovníku. Položky Released slovníku a Cancelled akce odeberou.

Zpracování Moved akce je ale složitější. Pokud je zapojen pouze jeden prst, zpracování je velmi stejné jako předchozí program. U dvou nebo více prstů musí program také získat informace ze slovníku zahrnujícího prst, který se nepřehybuje. Provede to zkopírováním klíčů slovníku do pole a následným porovnáním prvního klíče s ID pohybujícího se prstem. To umožňuje programu získat bod otáčení odpovídající prstu, který se nepřesuhuje.

V dalším kroku program vypočítá dva vektory nové pozice prstu vzhledem k bodu otáčení a původní pozici prstu vzhledem k bodu otáčení. Poměry těchto vektorů jsou faktory škálování. Vzhledem k tomu, že dělení nulou je možnost, musí být tyto hodnoty kontrolovány pro nekonečné hodnoty nebo hodnoty NaN (nikoli číselné). Pokud je vše v pořádku, transformace škálování se zřetědí s SKMatrix hodnotou uloženou jako pole.

Při experimentování s touto stránkou si všimnete, že rastrový obrázek můžete přetáhnout jedním nebo dvěma prsty nebo změnit jeho měřítko dvěma prsty. Měřítko je anisotropní, což znamená, že měřítko se může lišit ve vodorovném a svislém směru. To zkresluje poměr stran, ale také umožňuje překlopit rastrový obrázek, aby se zrcadlový obrázek. Můžete také zjistit, že rastrový obrázek můžete zmenšit na nulovou dimenzi a zmizí. V produkčním kódu budete chtít chránit před tím.

Otočení dvěma prsty

Stránka Rastrové otočení umožňuje použít dva prsty pro otočení nebo isotropní měřítko. Rastrový obrázek vždy zachovává správný poměr stran. Použití dvou prstů pro otočení i anisotropní měřítko nefunguje velmi dobře, protože pohyb prstů je velmi podobný pro oba úkoly.

Prvním velkým rozdílem v tomto programu je logika testování hit-testování. Předchozí programy použily metodu Contains SKRect k určení, zda je dotykový bod v transformované obdélník, který odpovídá bitmapu. Když ale uživatel pracuje s rastrovým obrázkem, může být rastrový obrázek otočený a SKRect nemůže správně reprezentovat otočený obdélník. Možná se bojíte, že logika hit-testování musí v takovém případě implementovat poměrně složitou analytickou geometrii.

Je však k dispozici zkratka: Určení, zda bod leží v mezích transformovaného obdélníku, je stejný jako určení, zda inverzní transformovaný bod leží v rámci hranic nepřepraveného obdélníku. To je mnohem jednodušší výpočet a logika může i nadále používat pohodlnou Contains metodu:

public partial class BitmapRotationPage : ContentPage
{
    ···
    // Touch information
    Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!touchDictionary.ContainsKey(args.Id))
                {
                    // Invert the matrix
                    if (matrix.TryInvert(out SKMatrix inverseMatrix))
                    {
                        // Transform the point using the inverted matrix
                        SKPoint transformedPoint = inverseMatrix.MapPoint(point);

                        // Check if it's in the untransformed bitmap rectangle
                        SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);

                        if (rect.Contains(transformedPoint))
                        {
                            touchDictionary.Add(args.Id, point);
                        }
                    }
                }
                break;

            case TouchActionType.Moved:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    // Single-finger drag
                    if (touchDictionary.Count == 1)
                    {
                        SKPoint prevPoint = touchDictionary[args.Id];

                        // Adjust the matrix for the new position
                        matrix.TransX += point.X - prevPoint.X;
                        matrix.TransY += point.Y - prevPoint.Y;
                        canvasView.InvalidateSurface();
                    }
                    // Double-finger rotate, scale, and drag
                    else if (touchDictionary.Count >= 2)
                    {
                        // Copy two dictionary keys into array
                        long[] keys = new long[touchDictionary.Count];
                        touchDictionary.Keys.CopyTo(keys, 0);

                        // Find index non-moving (pivot) finger
                        int pivotIndex = (keys[0] == args.Id) ? 1 : 0;

                        // Get the three points in the transform
                        SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                        SKPoint prevPoint = touchDictionary[args.Id];
                        SKPoint newPoint = point;

                        // Calculate two vectors
                        SKPoint oldVector = prevPoint - pivotPoint;
                        SKPoint newVector = newPoint - pivotPoint;

                        // Find angles from pivot point to touch points
                        float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                        float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                        // Calculate rotation matrix
                        float angle = newAngle - oldAngle;
                        SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                        // Effectively rotate the old vector
                        float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                        oldVector.X = magnitudeRatio * newVector.X;
                        oldVector.Y = magnitudeRatio * newVector.Y;

                        // Isotropic scaling!
                        float scale = Magnitude(newVector) / Magnitude(oldVector);

                        if (!float.IsNaN(scale) && !float.IsInfinity(scale))
                        {
                            SKMatrix.PostConcat(ref touchMatrix,
                                SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));

                            SKMatrix.PostConcat(ref matrix, touchMatrix);
                            canvasView.InvalidateSurface();
                        }
                    }

                    // Store the new point in the dictionary
                    touchDictionary[args.Id] = point;
                }

                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Remove(args.Id);
                }
                break;
        }
    }

    float Magnitude(SKPoint point)
    {
        return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
    }
    ···
}

Logika události Moved začíná stejně jako předchozí program. Dva vektory pojmenované oldVector a newVector jsou vypočítány na základě předchozího a aktuálního bodu pohybujícího se prstu a bodu otáčení neovlivujícího prstu. Pak se ale určují úhly těchto vektorů a rozdíl je úhel otáčení.

Měřítko může být také zapojeno, takže starý vektor se otočí na základě úhlu otočení. Relativní velikost těchto dvou vektorů je teď faktorem měřítka. Všimněte si, že stejná scale hodnota se používá pro horizontální a vertikální škálování tak, aby škálování je isotropní. Pole matrix se upraví maticí otočení i maticí měřítka.

Pokud vaše aplikace potřebuje implementovat dotykové zpracování pro jeden rastrový obrázek (nebo jiný objekt), můžete přizpůsobit kód z těchto tří ukázek pro vlastní aplikaci. Pokud ale potřebujete implementovat dotykové zpracování pro více rastrových obrázků, budete pravděpodobně chtít zapouzdřovat tyto dotykové operace v jiných třídách.

Zapouzdření operací dotykového ovládání

Stránka Manipulace s dotykem ukazuje manipulaci s dotykem jednoho rastrového obrázku, ale pomocí několika dalších souborů, které zapouzdřují většinu logiky uvedené výše. První z těchto souborů je TouchManipulationMode výčet, který označuje různé typy manipulace s dotykem implementovaný kódem, který uvidíte:

enum TouchManipulationMode
{
    None,
    PanOnly,
    IsotropicScale,     // includes panning
    AnisotropicScale,   // includes panning
    ScaleRotate,        // implies isotropic scaling
    ScaleDualRotate     // adds one-finger rotation
}

PanOnly je jedním prstem, který je implementován s překladem. Všechny následující možnosti také zahrnují posouvání, ale zahrnují dva prsty: IsotropicScale je operace stažení, která má za následek rovnoměrné škálování objektu ve vodorovném a svislém směru. AnisotropicScale umožňuje nerovné škálování.

Tato možnost je určená ScaleRotate pro změnu velikosti a otočení dvou prsty. Škálování je isotropní. Jak už bylo zmíněno dříve, implementace otočení dvěma prsty s anisotropní škálování je problematické, protože pohyby prstu jsou v podstatě stejné.

Tato ScaleDualRotate možnost přidá otočení jedním prstem. Když objekt přetáhnete jedním prstem, přetažený objekt se nejprve otočí kolem středu objektu tak, aby se střed objektu čaroval s přetahováním vektoru.

Soubor TouchManipulationPage.xaml obsahuje Picker členy výčtu TouchManipulationMode :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
             x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
             Title="Touch Manipulation">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker Title="Touch Mode"
                Grid.Row="0"
                SelectedIndexChanged="OnTouchModePickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type local:TouchManipulationMode}">
                    <x:Static Member="local:TouchManipulationMode.None" />
                    <x:Static Member="local:TouchManipulationMode.PanOnly" />
                    <x:Static Member="local:TouchManipulationMode.IsotropicScale" />
                    <x:Static Member="local:TouchManipulationMode.AnisotropicScale" />
                    <x:Static Member="local:TouchManipulationMode.ScaleRotate" />
                    <x:Static Member="local:TouchManipulationMode.ScaleDualRotate" />
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                4
            </Picker.SelectedIndex>
        </Picker>
        
        <Grid BackgroundColor="White"
              Grid.Row="1">
            
            <skia:SKCanvasView x:Name="canvasView"
                               PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>
    </Grid>
</ContentPage>

V dolní části je SKCanvasView buňka a připojená TouchEffect k jedné buňce Grid , která ji uzavře.

Soubor TouchManipulationPage.xaml.cs kódu má bitmap pole, ale není typu SKBitmap. Typ je TouchManipulationBitmap (krátce uvidíte třídu):

public partial class TouchManipulationPage : ContentPage
{
    TouchManipulationBitmap bitmap;
    ...

    public TouchManipulationPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            SKBitmap bitmap = SKBitmap.Decode(stream);
            this.bitmap = new TouchManipulationBitmap(bitmap);
            this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
        }
    }
    ...
}

Konstruktor vytvoří TouchManipulationBitmap instanci objektu, který předá konstruktoru SKBitmap získanému z vloženého prostředku. Konstruktor končí nastavením Mode vlastnosti objektu TouchManipulationBitmap na člena výčtu TouchManipulationMode TouchManager.

Obslužná rutina SelectedIndexChanged pro Picker také nastaví tuto Mode vlastnost:

public partial class TouchManipulationPage : ContentPage
{
    ...
    void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (bitmap != null)
        {
            Picker picker = (Picker)sender;
            bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
        }
    }
    ...
}

Obslužná TouchAction rutina TouchEffect instance v souboru XAML volá dvě metody s TouchManipulationBitmap názvem HitTest a ProcessTouchEvent:

public partial class TouchManipulationPage : ContentPage
{
    ...
    List<long> touchIds = new List<long>();
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (bitmap.HitTest(point))
                {
                    touchIds.Add(args.Id);
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    break;
                }
                break;

            case TouchActionType.Moved:
                if (touchIds.Contains(args.Id))
                {
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchIds.Contains(args.Id))
                {
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    touchIds.Remove(args.Id);
                    canvasView.InvalidateSurface();
                }
                break;
        }
    }
    ...
}

Pokud metoda HitTest vrátí true , což znamená, že prst se dotkne obrazovky v oblasti obsazené rastrovým obrázkem , pak se do kolekce přidá TouchIds touch ID. Toto ID představuje posloupnost dotykových událostí pro daný prst, dokud prst nezvedne z obrazovky. Pokud se rastrový obrázek dotkne více prstů, touchIds kolekce obsahuje touch ID pro každý prst.

Obslužná rutina TouchAction také volá ProcessTouchEvent třídu v TouchManipulationBitmap. To je místo, kde dochází k některým (ale ne všem) skutečného zpracování dotykového ovládání.

Třída TouchManipulationBitmap je obálková třída, která SKBitmap obsahuje kód pro vykreslení rastrového obrázku a zpracování událostí dotykového ovládání. Funguje ve spojení s obecnějším kódem TouchManipulationManager ve třídě (který uvidíte za chvíli).

Konstruktor TouchManipulationBitmap uloží SKBitmap a vytvoří instanci dvou vlastností, TouchManager vlastnost typu TouchManipulationManager a Matrix vlastnost typu SKMatrix:

class TouchManipulationBitmap
{
    SKBitmap bitmap;
    ...

    public TouchManipulationBitmap(SKBitmap bitmap)
    {
        this.bitmap = bitmap;
        Matrix = SKMatrix.MakeIdentity();

        TouchManager = new TouchManipulationManager
        {
            Mode = TouchManipulationMode.ScaleRotate
        };
    }

    public TouchManipulationManager TouchManager { set; get; }

    public SKMatrix Matrix { set; get; }
    ...
}

Tato Matrix vlastnost je kumulovaná transformace, která je výsledkem veškeré dotykové aktivity. Jak vidíte, každá dotyková událost se přeloží do matice, která se pak zřetědí s SKMatrix hodnotou uloženou vlastností Matrix .

Objekt TouchManipulationBitmap se nakreslí ve své Paint metodě. Argument je SKCanvas objekt. Tato SKCanvas transformace už může mít použitou transformaci, takže Paint metoda zřetězí Matrix vlastnost přidruženou k bitmapě k existující transformaci a obnoví plátno po dokončení:

class TouchManipulationBitmap
{
    ...
    public void Paint(SKCanvas canvas)
    {
        canvas.Save();
        SKMatrix matrix = Matrix;
        canvas.Concat(ref matrix);
        canvas.DrawBitmap(bitmap, 0, 0);
        canvas.Restore();
    }
    ...
}

Metoda HitTest vrátí true , pokud se uživatel dotkne obrazovky v bodě v rámci hranic rastrového obrázku. Používá se logika zobrazená dříve na stránce Otočení rastrového obrázku:

class TouchManipulationBitmap
{
    ...
    public bool HitTest(SKPoint location)
    {
        // Invert the matrix
        SKMatrix inverseMatrix;

        if (Matrix.TryInvert(out inverseMatrix))
        {
            // Transform the point using the inverted matrix
            SKPoint transformedPoint = inverseMatrix.MapPoint(location);

            // Check if it's in the untransformed bitmap rectangle
            SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
            return rect.Contains(transformedPoint);
        }
        return false;
    }
    ...
}

Druhá veřejná metoda TouchManipulationBitmap je ProcessTouchEvent. Při zavolání této metody již bylo zjištěno, že dotyková událost patří do této konkrétní bitmapy. Metoda udržuje slovník TouchManipulationInfo objektů, což je jednoduše předchozí bod a nový bod každého prstu:

class TouchManipulationInfo
{
    public SKPoint PreviousPoint { set; get; }

    public SKPoint NewPoint { set; get; }
}

Tady je slovník a ProcessTouchEvent samotná metoda:

class TouchManipulationBitmap
{
    ...
    Dictionary<long, TouchManipulationInfo> touchDictionary =
        new Dictionary<long, TouchManipulationInfo>();
    ...
    public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
    {
        switch (type)
        {
            case TouchActionType.Pressed:
                touchDictionary.Add(id, new TouchManipulationInfo
                {
                    PreviousPoint = location,
                    NewPoint = location
                });
                break;

            case TouchActionType.Moved:
                TouchManipulationInfo info = touchDictionary[id];
                info.NewPoint = location;
                Manipulate();
                info.PreviousPoint = info.NewPoint;
                break;

            case TouchActionType.Released:
                touchDictionary[id].NewPoint = location;
                Manipulate();
                touchDictionary.Remove(id);
                break;

            case TouchActionType.Cancelled:
                touchDictionary.Remove(id);
                break;
        }
    }
    ...
}

V událostech Moved a Released událostech metoda volá Manipulate. V těchto případech touchDictionary obsahuje jeden nebo více TouchManipulationInfo objektů. touchDictionary Pokud obsahuje jednu položku, je pravděpodobné, že PreviousPoint hodnoty jsou NewPoint nerovné a představují pohyb prstu. Pokud se rastrový obrázek dotkne více prstů, obsahuje slovník více než jednu položku, ale pouze jedna z těchto položek má jiné PreviousPoint hodnoty a NewPoint hodnoty. Všechny ostatní mají stejné PreviousPoint hodnoty a NewPoint hodnoty.

To je důležité: Metoda Manipulate může předpokládat, že zpracovává pohyb pouze jednoho prstu. V době tohoto volání se žádný z ostatních prstů nepřehýbá, a pokud skutečně pohybují (jak je pravděpodobné), tyto pohyby budou zpracovány v budoucích voláních Manipulate.

Metoda Manipulate nejprve zkopíruje slovník do pole pro usnadnění. Ignoruje cokoli jiného než první dvě položky. Pokud se rastrový obrázek pokouší manipulovat více než dvěma prsty, ostatní se ignorují. Manipulate je posledním členem TouchManipulationBitmap:

class TouchManipulationBitmap
{
    ...
    void Manipulate()
    {
        TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
        touchDictionary.Values.CopyTo(infos, 0);
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();

        if (infos.Length == 1)
        {
            SKPoint prevPoint = infos[0].PreviousPoint;
            SKPoint newPoint = infos[0].NewPoint;
            SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);

            touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
        }
        else if (infos.Length >= 2)
        {
            int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
            SKPoint pivotPoint = infos[pivotIndex].NewPoint;
            SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
            SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;

            touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
        }

        SKMatrix matrix = Matrix;
        SKMatrix.PostConcat(ref matrix, touchMatrix);
        Matrix = matrix;
    }
}

Pokud jeden prst pracuje s rastrem, Manipulate volá OneFingerManipulate metodu objektu TouchManipulationManager . Pro dva prsty volá TwoFingerManipulate. Argumenty těchto metod jsou stejné: prevPoint argumenty newPoint představují prst, který se přesouvá. pivotPoint Ale argument se u těchto dvou volání liší:

Pro manipulaci pivotPoint jedním prstem je střed rastrového obrázku. To umožňuje otočení jedním prstem. U manipulace se dvěma prsty označuje událost pohyb pouze jednoho prstu, takže pivotPoint je to prst, který se nepřehybuje.

V obou případech TouchManipulationManager vrátí SKMatrix hodnotu, která metoda zřetězí s aktuální Matrix vlastností, která TouchManipulationPage používá k vykreslení rastrového obrázku.

TouchManipulationManager je zobecněn a nepoužívá žádné jiné soubory s výjimkou TouchManipulationMode. Tuto třídu můžete používat beze změny ve vlastních aplikacích. Definuje jednu vlastnost typu TouchManipulationMode:

class TouchManipulationManager
{
    public TouchManipulationMode Mode { set; get; }
    ...
}

Pravděpodobně se ale budete chtít této možnosti vyhnout AnisotropicScale . S touto možností je velmi snadné manipulovat s rastrovým obrázkem, aby se jeden z faktorů škálování stala nulou. Díky tomu rastrový obrázek zmizí z dohledu, nikdy se nevrátí. Pokud skutečně potřebujete anisotropní škálování, budete chtít vylepšit logiku, abyste se vyhnuli nežádoucím výsledkům.

TouchManipulationManager používá vektory, ale vzhledem k tomu, že v SkiaSharpu není žádná SKVector struktura, SKPoint se místo toho používá. SKPoint podporuje operátor odčítání a výsledek lze považovat za vektor. Jedinou vektorovou logikou Magnitude , kterou je potřeba přidat, je výpočet:

class TouchManipulationManager
{
    ...
    float Magnitude(SKPoint point)
    {
        return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
    }
}

Pokaždé, když je vybráno otočení, zpracují se jako první metody manipulace s jedním prstem i dvěma prsty. Pokud se zjistí nějaká rotace, komponenta rotace se efektivně odebere. To, co zůstává, se interpretuje jako posouvání a škálování.

Tady je OneFingerManipulate metoda. Pokud není povoleno otočení jedním prstem, je logika jednoduchá – jednoduše používá předchozí bod a nový bod k vytvoření vektoru s názvem delta , který přesně odpovídá překladu. Při povoleném otočení jedním prstem používá metoda úhely z bodu otáčení (střed rastrového obrázku) na předchozí bod a nový bod k vytvoření matice otočení:

class TouchManipulationManager
{
    public TouchManipulationMode Mode { set; get; }

    public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        if (Mode == TouchManipulationMode.None)
        {
            return SKMatrix.MakeIdentity();
        }

        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint delta = newPoint - prevPoint;

        if (Mode == TouchManipulationMode.ScaleDualRotate)  // One-finger rotation
        {
            SKPoint oldVector = prevPoint - pivotPoint;
            SKPoint newVector = newPoint - pivotPoint;

            // Avoid rotation if fingers are too close to center
            if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
            {
                float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                // Calculate rotation matrix
                float angle = newAngle - prevAngle;
                touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                // Effectively rotate the old vector
                float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                oldVector.X = magnitudeRatio * newVector.X;
                oldVector.Y = magnitudeRatio * newVector.Y;

                // Recalculate delta
                delta = newVector - oldVector;
            }
        }

        // Multiply the rotation matrix by a translation matrix
        SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));

        return touchMatrix;
    }
    ...
}

TwoFingerManipulate V metodě je kontingenční bod pozice prstu, který se v této konkrétní události dotykového ovládání nepřesuhuje. Otočení je velmi podobné otočení jedním prstem a vektor pojmenovaný oldVector (na základě předchozího bodu) se upraví pro otočení. Zbývající přesun se interpretuje jako škálování:

class TouchManipulationManager
{
    ...
    public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint oldVector = prevPoint - pivotPoint;
        SKPoint newVector = newPoint - pivotPoint;

        if (Mode == TouchManipulationMode.ScaleRotate ||
            Mode == TouchManipulationMode.ScaleDualRotate)
        {
            // Find angles from pivot point to touch points
            float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
            float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

            // Calculate rotation matrix
            float angle = newAngle - oldAngle;
            touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

            // Effectively rotate the old vector
            float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
            oldVector.X = magnitudeRatio * newVector.X;
            oldVector.Y = magnitudeRatio * newVector.Y;
        }

        float scaleX = 1;
        float scaleY = 1;

        if (Mode == TouchManipulationMode.AnisotropicScale)
        {
            scaleX = newVector.X / oldVector.X;
            scaleY = newVector.Y / oldVector.Y;

        }
        else if (Mode == TouchManipulationMode.IsotropicScale ||
                 Mode == TouchManipulationMode.ScaleRotate ||
                 Mode == TouchManipulationMode.ScaleDualRotate)
        {
            scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
        }

        if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
            !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
        {
            SKMatrix.PostConcat(ref touchMatrix,
                SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
        }

        return touchMatrix;
    }
    ...
}

Všimněte si, že tato metoda neobsahuje explicitní překlad. Obě metody MakeRotation MakeScale jsou však založeny na kontingenčním bodu a zahrnují implicitní překlad. Pokud na rastrovém obrázku používáte dva prsty a přetáhnete je stejným směrem, získáte řadu dotykových událostí, TouchManipulation které se střídají mezi dvěma prsty. Když se každý prst pohybuje vzhledem k druhému, škálování nebo otočení výsledků, ale je negovaný pohybem druhého prstu a výsledek je překlad.

Jedinou zbývající částí stránky Manipulace s dotykovým ovládáním PaintSurface je obslužná rutina v TouchManipulationPage souboru kódu. To volá Paint metodu TouchManipulationBitmap, která aplikuje matici představující kumulovanou dotykovou aktivitu:

public partial class TouchManipulationPage : ContentPage
{
    ...
    MatrixDisplay matrixDisplay = new MatrixDisplay();
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        bitmap.Paint(canvas);

        // Display the matrix in the lower-right corner
        SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);

        matrixDisplay.Paint(canvas, bitmap.Matrix,
            new SKPoint(info.Width - matrixSize.Width,
                        info.Height - matrixSize.Height));
    }
}

Obslužná rutina PaintSurface končí zobrazením objektu zobrazující kumulovanou dotykovou MatrixDisplay matici:

Trojitý snímek obrazovky se stránkou Manipulace s dotykovým ovládáním

Manipulace s více rastrovými obrázky

Jednou z výhod izolace kódu dotykového zpracování ve třídách, jako TouchManipulationBitmap TouchManipulationManager je a je schopnost opakovaně používat tyto třídy v programu, který uživateli umožňuje manipulovat s více rastrovými obrázky.

Stránka Rastrové bodové zobrazení ukazuje, jak se to dělá. Místo definování pole typu TouchManipulationBitmapBitmapScatterPage třída definuje List rastrové objekty:

public partial class BitmapScatterViewPage : ContentPage
{
    List<TouchManipulationBitmap> bitmapCollection =
        new List<TouchManipulationBitmap>();
    ...
    public BitmapScatterViewPage()
    {
        InitializeComponent();

        // Load in all the available bitmaps
        Assembly assembly = GetType().GetTypeInfo().Assembly;
        string[] resourceIDs = assembly.GetManifestResourceNames();
        SKPoint position = new SKPoint();

        foreach (string resourceID in resourceIDs)
        {
            if (resourceID.EndsWith(".png") ||
                resourceID.EndsWith(".jpg"))
            {
                using (Stream stream = assembly.GetManifestResourceStream(resourceID))
                {
                    SKBitmap bitmap = SKBitmap.Decode(stream);
                    bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
                    {
                        Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
                    });
                    position.X += 100;
                    position.Y += 100;
                }
            }
        }
    }
    ...
}

Konstruktor se načte do všech rastrových obrázků, které jsou k dispozici jako vložené prostředky, a přidá je do bitmapCollection. Všimněte si, že Matrix vlastnost je inicializována u každého TouchManipulationBitmap objektu, takže levé horní rohy každého rastrového obrázku jsou posunuty o 100 pixelů.

Stránka BitmapScatterView také potřebuje zpracovat dotykové události pro více rastrových obrázků. Místo definování List dotykových ID aktuálně manipulovaných TouchManipulationBitmap objektů tento program vyžaduje slovník:

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
       new Dictionary<long, TouchManipulationBitmap>();
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                for (int i = bitmapCollection.Count - 1; i >= 0; i--)
                {
                    TouchManipulationBitmap bitmap = bitmapCollection[i];

                    if (bitmap.HitTest(point))
                    {
                        // Move bitmap to end of collection
                        bitmapCollection.Remove(bitmap);
                        bitmapCollection.Add(bitmap);

                        // Do the touch processing
                        bitmapDictionary.Add(args.Id, bitmap);
                        bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                        canvasView.InvalidateSurface();
                        break;
                    }
                }
                break;

            case TouchActionType.Moved:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    bitmapDictionary.Remove(args.Id);
                    canvasView.InvalidateSurface();
                }
                break;
        }
    }
    ...
}

Všimněte si, jak Pressed logika prochází obráceně bitmapCollection . Rastrové obrázky se často vzájemně překrývají. Rastrové obrázky později v kolekci vizuálně leží nad rastrovými obrázky dříve v kolekci. Pokud je pod prstem více rastrových obrázků, které se stisknou na obrazovce, musí být první rastrový obrázek, který je manipulován tímto prstem.

Všimněte si také, že logika Pressed přesune rastrový obrázek na konec kolekce, aby se vizuálně přesunula na začátek hromady jiných rastrových obrázků.

Moved V událostech a Released obslužná rutina TouchAction volá metodu ProcessingTouchEvent stejně TouchManipulationBitmap jako předchozí program.

Nakonec obslužná rutina PaintSurface volá metodu každého TouchManipulationBitmap objektuPaint:

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKCanvas canvas = args.Surface.Canvas;
        canvas.Clear();

        foreach (TouchManipulationBitmap bitmap in bitmapCollection)
        {
            bitmap.Paint(canvas);
        }
    }
}

Kód prochází kolekcí a zobrazuje hromádku rastrových obrázků od začátku kolekce až po konec:

Trojitý snímek obrazovky se stránkou Rastrové bodové zobrazení

Měřítko jedním prstem

Operace škálování obecně vyžaduje gesto stažení dvěma prsty. Škálování je však možné implementovat jedním prstem tak, že prstem posune rohy rastrového obrázku.

To je znázorněno na stránce Měřítko s jedním prstem. Vzhledem k tomu, že tato ukázka používá poněkud jiný typ škálování než implementovaný ve TouchManipulationManager třídě, nepoužívá tuto třídu ani TouchManipulationBitmap třídu. Místo toho je veškerá dotyková logika v souboru kódu. To je poněkud jednodušší logika než obvykle, protože sleduje vždy jenom jeden prst a jednoduše ignoruje všechny sekundární prsty, které se můžou dotýkat obrazovky.

Stránka SingleFingerCornerScale.xaml vytvoří SKCanvasView instanci třídy a vytvoří TouchEffect objekt pro sledování dotykových událostí:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
             Title="Single Finger Corner Scale">

    <Grid BackgroundColor="White"
          Grid.Row="1">

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction"   />
        </Grid.Effects>
    </Grid>
</ContentPage>

Soubor SingleFingerCornerScalePage.xaml.cs načte rastrový prostředek z adresáře Médií a zobrazí ho pomocí objektu SKMatrix definovaného jako pole:

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();
    ···

    public SingleFingerCornerScalePage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.SetMatrix(currentMatrix);
        canvas.DrawBitmap(bitmap, 0, 0);
    }
    ···
}

Tento SKMatrix objekt je upraven dotykovou logikou zobrazenou níže.

Zbytek souboru kódu je obslužná rutina TouchEffect události. Začíná převodem aktuálního umístění prstu na SKPoint hodnotu. Pressed U typu akce obslužná rutina zkontroluje, že se na obrazovce nedotýká žádný jiný prst a že je prst v mezích rastrového obrázku.

Klíčovou součástí kódu je if příkaz zahrnující dvě volání Math.Pow metody. Tato matematika zkontroluje, jestli je umístění prstu mimo tři tečky, které vyplní rastrový obrázek. Pokud ano, znamená to operace škálování. Prst je blízko jednoho z rohů rastrového obrázku a bod otáčení je určen, že je opačný roh. Pokud je prst v tomto elipse, jedná se o běžnou operaci posouvání:

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();

    // Information for translating and scaling
    long? touchId = null;
    SKPoint pressedLocation;
    SKMatrix pressedMatrix;

    // Information for scaling
    bool isScaling;
    SKPoint pivotPoint;
    ···

    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Track only one finger
                if (touchId.HasValue)
                    return;

                // Check if the finger is within the boundaries of the bitmap
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = currentMatrix.MapRect(rect);
                if (!rect.Contains(point))
                    return;

                // First assume there will be no scaling
                isScaling = false;

                // If touch is outside interior ellipse, make this a scaling operation
                if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
                    Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
                {
                    isScaling = true;
                    float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
                    float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
                    pivotPoint = new SKPoint(xPivot, yPivot);
                }

                // Common for either pan or scale
                touchId = args.Id;
                pressedLocation = point;
                pressedMatrix = currentMatrix;
                break;

            case TouchActionType.Moved:
                if (!touchId.HasValue || args.Id != touchId.Value)
                    return;

                SKMatrix matrix = SKMatrix.MakeIdentity();

                // Translating
                if (!isScaling)
                {
                    SKPoint delta = point - pressedLocation;
                    matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
                }
                // Scaling
                else
                {
                    float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
                    float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
                    matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
                }

                // Concatenate the matrices
                SKMatrix.PreConcat(ref matrix, pressedMatrix);
                currentMatrix = matrix;
                canvasView.InvalidateSurface();
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = null;
                break;
        }
    }
}

Typ Moved akce vypočítá matici odpovídající dotykové aktivitě od okamžiku, kdy prst po stisknutí prstu až do této doby stiskl obrazovku. Zřetězí matici s maticí v okamžiku, kdy prst poprvé stiskl rastrový obrázek. Operace škálování je vždy relativní vzhledem k rohu proti rohu, na který se prst dotkl.

U malých nebo podlouhlých rastrových obrázků může vnitřní elipsa zabírat většinu rastrového obrázku a nechat malé oblasti v rozích, aby bylo možné rastrový obrázek škálovat. Můžete preferovat poněkud jiný přístup, v takovém případě můžete nahradit celý if blok, který se nastaví isScaling na true tento kód:

float halfHeight = rect.Height / 2;
float halfWidth = rect.Width / 2;

// Top half of bitmap
if (point.Y < rect.MidY)
{
    float yRelative = (point.Y - rect.Top) / halfHeight;

    // Upper-left corner
    if (point.X < rect.MidX - yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Right, rect.Bottom);
    }
    // Upper-right corner
    else if (point.X > rect.MidX + yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Left, rect.Bottom);
    }
}
// Bottom half of bitmap
else
{
    float yRelative = (point.Y - rect.MidY) / halfHeight;

    // Lower-left corner
    if (point.X < rect.Left + yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Right, rect.Top);
    }
    // Lower-right corner
    else if (point.X > rect.Right - yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Left, rect.Top);
    }
}

Tento kód efektivně rozdělí oblast rastrového obrázku na vnitřní kosočtverec a čtyři trojúhelníky v rozích. To umožňuje mnohem větší oblasti v rozích zachytit a škálovat rastrový obrázek.