Manipolazioni tramite tocco

Download Sample Scaricare l'esempio

Usare le trasformazioni della matrice per implementare il trascinamento del tocco, il avvicinamento delle dita e la rotazione

Negli ambienti multitocco, ad esempio quelli nei dispositivi mobili, gli utenti spesso usano le dita per modificare gli oggetti sullo schermo. Movimenti comuni, ad esempio un trascinamento con un dito e un avvicinamento con due dita, possono spostare e ridimensionare oggetti o anche ruotarli. Questi movimenti vengono in genere implementati usando matrici di trasformazione e questo articolo illustra come eseguire questa operazione.

A bitmap subjected to translation, scaling, and rotation

Tutti gli esempi illustrati qui usano l'effetto Xamarin.Forms di rilevamento del tocco presentato nell'articolo Richiamo di eventi da Effetti.

Trascinamento e traduzione

Una delle applicazioni più importanti delle trasformazioni della matrice è l'elaborazione del tocco. Un singolo SKMatrix valore può consolidare una serie di operazioni di tocco.

Per il trascinamento con un solo dito, il valore esegue la SKMatrix conversione. Questa operazione è illustrata nella pagina Di trascinamento bitmap. Il file XAML crea un'istanza SKCanvasView di in un oggetto Xamarin.FormsGrid. Un TouchEffect oggetto è stato aggiunto all'insieme Effects di :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>

In teoria, l'oggetto TouchEffect può essere aggiunto direttamente all'insieme Effects di SKCanvasView, ma non funziona su tutte le piattaforme. Poiché è SKCanvasView la stessa dimensione di Grid in questa configurazione, il collegamento all'oggetto Grid funziona altrettanto bene.

Il file code-behind viene caricato in una risorsa bitmap nel relativo costruttore e lo visualizza nel PaintSurface gestore:

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());
    }
}

Senza un altro codice, il SKMatrix valore è sempre la matrice di identificazione e non avrà alcun effetto sulla visualizzazione della bitmap. L'obiettivo del OnTouchEffectAction gestore impostato nel file XAML consiste nel modificare il valore della matrice in modo da riflettere le manipolazioni del tocco.

Il OnTouchEffectAction gestore inizia convertendo il Xamarin.FormsPoint valore in un valore SkiaSharp SKPoint . Si tratta di una semplice questione di ridimensionamento in base alle Width proprietà e Height di SKCanvasView (che sono unità indipendenti dal dispositivo) e alla CanvasSize proprietà, espressa in unità di 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;
        }
    }
    ···
}

Quando un dito tocca per la prima volta lo schermo, viene generato un evento di tipo TouchActionType.Pressed . La prima attività consiste nel determinare se il dito tocca la bitmap. Un'attività di questo tipo viene spesso chiamata hit testing. In questo caso, è possibile eseguire l'hit testing creando un SKRect valore corrispondente alla bitmap, applicando la trasformazione matrice a essa con MapRecte quindi determinando se il punto di tocco si trova all'interno del rettangolo trasformato.

In questo caso, il touchId campo viene impostato sull'ID tocco e la posizione del dito viene salvata.

Per l'evento TouchActionType.Moved , i fattori di traslazione del SKMatrix valore vengono regolati in base alla posizione corrente del dito e alla nuova posizione del dito. La nuova posizione viene salvata per la volta successiva e l'oggetto SKCanvasView viene invalidato.

Durante l'esperimento con questo programma, tieni presente che puoi trascinare la bitmap solo quando il dito tocca un'area in cui viene visualizzata la bitmap. Anche se questa restrizione non è molto importante per questo programma, diventa fondamentale quando si modificano più bitmap.

Avvicinamento delle dita e ridimensionamento

Cosa vuoi accadere quando due dita toccano la bitmap? Se le due dita si spostano in parallelo, è probabile che la bitmap si muova insieme alle dita. Se le due dita eseguono un'operazione di avvicinamento o di estensione, è possibile che la bitmap venga ruotata (da discutere nella sezione successiva) o ridimensionata. Quando si ridimensiona una bitmap, è più opportuno che le due dita rimangano nelle stesse posizioni rispetto alla bitmap e che la bitmap venga ridimensionata di conseguenza.

La gestione di due dita contemporaneamente sembra complicata, ma tenere presente che il TouchAction gestore riceve solo informazioni su un dito alla volta. Se due dita stanno manipolando la bitmap, per ogni evento, un dito ha cambiato posizione, ma l'altro non è cambiato. Nel codice della pagina Ridimensionamento bitmap riportato di seguito, il dito che non ha modificato la posizione viene chiamato punto pivot perché la trasformazione è relativa a quel punto.

Una differenza tra questo programma e il programma precedente è che è necessario salvare più ID tocco. A questo scopo viene usato un dizionario, in cui l'ID tocco è la chiave del dizionario e il valore del dizionario è la posizione corrente del dito:

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;
        }
    }
    ···
}

La gestione dell'azione Pressed è quasi identica al programma precedente, ad eccezione del fatto che l'ID e il punto di tocco vengono aggiunti al dizionario. Le Released azioni e Cancelled rimuovono la voce del dizionario.

La gestione dell'azione Moved è tuttavia più complessa. Se c'è un solo dito coinvolto, l'elaborazione è molto uguale al programma precedente. Per due o più dita, il programma deve anche ottenere informazioni dal dizionario che coinvolge il dito che non si muove. A tale scopo, copiare le chiavi del dizionario in una matrice e quindi confrontare la prima chiave con l'ID del dito spostato. Ciò consente al programma di ottenere il punto pivot corrispondente al dito che non si muove.

Successivamente, il programma calcola due vettori della nuova posizione del dito rispetto al punto pivot e la posizione precedente del dito rispetto al punto pivot. I rapporti di questi vettori sono fattori di ridimensionamento. Poiché la divisione per zero è una possibilità, questi devono essere controllati per i valori infiniti o NaN (non un numero). Se tutto è corretto, una trasformazione di ridimensionamento viene concatenata con il SKMatrix valore salvato come campo.

Durante l'esperimento con questa pagina, si noterà che è possibile trascinare la bitmap con una o due dita o scalarla con due dita. Il ridimensionamento è anisotropico, il che significa che il ridimensionamento può essere diverso nelle direzioni orizzontali e verticali. In questo modo le proporzioni vengono distorte, ma consentono anche di capovolgere la bitmap per creare un'immagine mirror. Si potrebbe anche scoprire che è possibile compattare la bitmap a una dimensione zero e scompare. Nel codice di produzione è consigliabile proteggersi da questo problema.

Rotazione a due dita

La pagina Bitmap Rotate consente di usare due dita per la rotazione o la scala isotropica. La bitmap mantiene sempre le proporzioni corrette. L'uso di due dita per la rotazione e la scala anisotropica non funziona molto bene perché il movimento delle dita è molto simile per entrambe le attività.

La prima grande differenza in questo programma è la logica di hit testing. I programmi precedenti usavano il Contains metodo di SKRect per determinare se il punto di tocco si trova all'interno del rettangolo trasformato che corrisponde alla bitmap. Tuttavia, quando l'utente modifica la bitmap, la bitmap potrebbe essere ruotata e SKRect non può rappresentare correttamente un rettangolo ruotato. È possibile temere che la logica di hit testing debba implementare una geometria analitica piuttosto complessa in questo caso.

Tuttavia, è disponibile un collegamento: determinare se un punto si trova all'interno dei limiti di un rettangolo trasformato è uguale a determinare se un punto trasformato inversa si trova all'interno dei limiti del rettangolo non trasformato. Si tratta di un calcolo molto più semplice e la logica può continuare a usare il metodo pratico 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));
    }
    ···
}

La logica per l'evento Moved inizia come il programma precedente. Due vettori denominati oldVector e newVector vengono calcolati in base al punto precedente e al punto corrente del dito mobile e al punto pivot del dito senza movimento. Tuttavia, gli angoli di questi vettori vengono determinati e la differenza è l'angolo di rotazione.

Il ridimensionamento potrebbe anche essere coinvolto, quindi il vettore precedente viene ruotato in base all'angolo di rotazione. La grandezza relativa dei due vettori è ora il fattore di ridimensionamento. Si noti che lo stesso scale valore viene usato per la scalabilità orizzontale e verticale in modo che il ridimensionamento sia isotropico. Il matrix campo viene regolato sia dalla matrice di rotazione che da una matrice di scala.

Se l'applicazione deve implementare l'elaborazione del tocco per una singola bitmap (o un altro oggetto), è possibile adattare il codice di questi tre esempi per la propria applicazione. Tuttavia, se è necessario implementare l'elaborazione del tocco per più bitmap, è probabile che si voglia incapsulare queste operazioni di tocco in altre classi.

Incapsulamento delle operazioni di tocco

La pagina Manipolazione tocco illustra la manipolazione del tocco di una singola bitmap, ma l'uso di diversi altri file che incapsulano gran parte della logica illustrata in precedenza. Il primo di questi file è l'enumerazione TouchManipulationMode , che indica i diversi tipi di manipolazione del tocco implementati dal codice visualizzato:

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

PanOnly è un trascinamento con un dito implementato con la traduzione. Tutte le opzioni successive includono anche la panoramica, ma comportano due dita: IsotropicScale è un'operazione di avvicinamento delle dita che comporta il ridimensionamento dell'oggetto in modo uniforme nelle direzioni orizzontali e verticali. AnisotropicScale consente un ridimensionamento diverso.

L'opzione ScaleRotate è per il ridimensionamento e la rotazione a due dita. Il ridimensionamento è isotropico. Come accennato in precedenza, l'implementazione della rotazione a due dita con scala anisotropica è problematica perché i movimenti delle dita sono essenzialmente gli stessi.

L'opzione ScaleDualRotate aggiunge una rotazione di un dito. Quando un singolo dito trascina l'oggetto, l'oggetto trascinato viene prima ruotato intorno al suo centro in modo che il centro dell'oggetto si allinea con il vettore di trascinamento.

Il file TouchManipulationPage.xaml include un Picker oggetto con i membri dell'enumerazione 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>

Verso il basso è un oggetto SKCanvasView e un TouchEffect oggetto collegato alla cella Grid singola che lo racchiude.

Il file code-behind TouchManipulationPage.xaml.cs ha un bitmap campo, ma non è di tipo SKBitmap. Il tipo è TouchManipulationBitmap (una classe che verrà visualizzata a breve):

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;
        }
    }
    ...
}

Il costruttore crea un'istanza di un TouchManipulationBitmap oggetto, passando al costruttore un SKBitmap oggetto ottenuto da una risorsa incorporata. Il costruttore termina impostando la Mode proprietà della TouchManager proprietà dell'oggetto TouchManipulationBitmap su un membro dell'enumerazione TouchManipulationMode .

Il SelectedIndexChanged gestore per l'oggetto Picker imposta anche questa Mode proprietà:

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

Il TouchAction gestore dell'istanza TouchEffect creata nel file XAML chiama due metodi in TouchManipulationBitmap denominati HitTest e 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;
        }
    }
    ...
}

Se il HitTest metodo restituisce true , vale a dire che un dito ha toccato lo schermo all'interno dell'area occupata dalla bitmap, l'ID tocco viene aggiunto alla TouchIds raccolta. Questo ID rappresenta la sequenza di eventi di tocco per il dito fino a quando il dito non si solleva dallo schermo. Se più dita toccano la bitmap, la touchIds raccolta contiene un ID tocco per ogni dito.

Il TouchAction gestore chiama anche la ProcessTouchEvent classe in TouchManipulationBitmap. Questo è il luogo in cui si verificano alcune (ma non tutte) dell'elaborazione del tocco reale.

La TouchManipulationBitmap classe è una classe wrapper per SKBitmap che contiene codice per il rendering degli eventi di tocco bitmap ed elaborazione. Funziona in combinazione con codice più generalizzato in una TouchManipulationManager classe (che verrà visualizzata a breve).

Il TouchManipulationBitmap costruttore salva e crea un'istanza SKBitmap di due proprietà, la TouchManager proprietà di tipo TouchManipulationManager e la Matrix proprietà di tipo 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; }
    ...
}

Questa Matrix proprietà è la trasformazione accumulata risultante da tutte le attività di tocco. Come si vedrà, ogni evento di tocco viene risolto in una matrice, che viene quindi concatenata con il SKMatrix valore archiviato dalla Matrix proprietà .

L'oggetto TouchManipulationBitmap disegna se stesso nel relativo Paint metodo. L'argomento è un SKCanvas oggetto . Potrebbe SKCanvas essere già stata applicata una trasformazione, quindi il Paint metodo concatena la Matrix proprietà associata alla bitmap alla trasformazione esistente e ripristina l'area di disegno al termine dell'operazione:

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

Il HitTest metodo restituisce true se l'utente tocca lo schermo in un punto entro i limiti della bitmap. In questo modo viene usata la logica illustrata in precedenza nella pagina Rotazione bitmap:

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;
    }
    ...
}

Il secondo metodo pubblico in TouchManipulationBitmap è ProcessTouchEvent. Quando questo metodo viene chiamato, è già stato stabilito che l'evento touch appartiene a questa bitmap specifica. Il metodo gestisce un dizionario di TouchManipulationInfo oggetti, che è semplicemente il punto precedente e il nuovo punto di ogni dito:

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

    public SKPoint NewPoint { set; get; }
}

Ecco il dizionario e il ProcessTouchEvent metodo stesso:

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

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

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

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

Moved Negli eventi e Released il metodo chiama Manipulate. In questi casi, contiene touchDictionary uno o più TouchManipulationInfo oggetti. Se contiene touchDictionary un elemento, è probabile che i PreviousPoint valori e NewPoint siano diversi e rappresentino il movimento di un dito. Se più dita toccano la bitmap, il dizionario contiene più di un elemento, ma solo uno di questi elementi ha valori e NewPoint diversiPreviousPoint. Tutti gli altri hanno valori uguali PreviousPoint e NewPoint .

Questo è importante: il Manipulate metodo può presupporre che stia elaborando il movimento di un solo dito. Al momento di questa chiamata nessuna delle altre dita è in movimento e se sono effettivamente in movimento (come è probabile), tali movimenti verranno elaborati nelle chiamate future a Manipulate.

Il Manipulate metodo copia innanzitutto il dizionario in una matrice per praticità. Ignora qualsiasi elemento diverso dalle prime due voci. Se più di due dita tentano di modificare la bitmap, gli altri vengono ignorati. Manipulate è il membro finale di 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;
    }
}

Se un dito sta manipolando la bitmap, Manipulate chiama il OneFingerManipulate metodo dell'oggetto TouchManipulationManager . Per due dita, chiama TwoFingerManipulate. Gli argomenti di questi metodi sono gli stessi: gli prevPoint argomenti e newPoint rappresentano il dito che si sta spostando. Ma l'argomento pivotPoint è diverso per le due chiamate:

Per la manipolazione di un dito, è pivotPoint il centro della bitmap. Ciò consente la rotazione di un dito. Per la manipolazione a due dita, l'evento indica il movimento di un solo dito, in modo che sia pivotPoint il dito che non si muove.

In entrambi i casi restituisce TouchManipulationManager un SKMatrix valore, che il metodo concatena con la proprietà corrente Matrix che TouchManipulationPage usa per eseguire il rendering della bitmap.

TouchManipulationManager è generalizzato e non usa altri file ad eccezione TouchManipulationModedi . Potrebbe essere possibile usare questa classe senza modifiche nelle proprie applicazioni. Definisce una singola proprietà di tipo TouchManipulationMode:

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

Tuttavia, è probabile che si voglia evitare l'opzione AnisotropicScale . È molto facile con questa opzione modificare la bitmap in modo che uno dei fattori di ridimensionamento diventi zero. In questo modo la bitmap scompare dalla vista, non torna mai. Se è veramente necessario un ridimensionamento anisotropico, è necessario migliorare la logica per evitare risultati indesiderati.

TouchManipulationManager utilizza vettori, ma poiché non esiste alcuna SKVector struttura in SkiaSharp, SKPoint viene invece usato. SKPoint supporta l'operatore di sottrazione e il risultato può essere considerato come vettore. L'unica logica specifica del vettore da aggiungere è un Magnitude calcolo:

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

Ogni volta che è stata selezionata la rotazione, entrambi i metodi di manipolazione con un dito e due dita gestiscono prima la rotazione. Se viene rilevata una rotazione, il componente di rotazione viene effettivamente rimosso. Ciò che rimane viene interpretato come panoramica e ridimensionamento.

Ecco il OneFingerManipulate metodo . Se la rotazione di un dito non è stata abilitata, la logica è semplice, ma usa semplicemente il punto precedente e il nuovo punto per costruire un vettore denominato delta che corrisponde esattamente alla traduzione. Con la rotazione con un dito abilitato, il metodo usa angoli dal punto pivot (il centro della bitmap) al punto precedente e al nuovo punto per costruire una matrice di rotazione:

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 Nel metodo il punto pivot è la posizione del dito che non si sposta in questo particolare evento di tocco. La rotazione è molto simile alla rotazione di un dito e quindi il vettore denominato oldVector (in base al punto precedente) viene regolato per la rotazione. Il movimento rimanente viene interpretato come ridimensionamento:

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;
    }
    ...
}

Si noterà che in questo metodo non è presente alcuna traduzione esplicita. Tuttavia, entrambi i MakeRotation metodi e MakeScale sono basati sul punto pivot e che includono la traduzione implicita. Se si usano due dita sulla bitmap e trascinandoli nella stessa direzione, TouchManipulation si otterrà una serie di eventi di tocco alternati tra le due dita. Man mano che ogni dito si sposta rispetto all'altro, il ridimensionamento o i risultati della rotazione, ma viene negato dal movimento dell'altro dito e il risultato è la traslazione.

L'unica parte rimanente della pagina Manipolazione tocco è il PaintSurface gestore nel TouchManipulationPage file code-behind. Viene chiamato il Paint metodo di TouchManipulationBitmap, che applica la matrice che rappresenta l'attività di tocco accumulata:

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));
    }
}

Il PaintSurface gestore termina visualizzando un MatrixDisplay oggetto che mostra la matrice di tocco accumulata:

Triple screenshot of the Touch Manipulation page

Modifica di più bitmap

Uno dei vantaggi dell'isolamento del codice di elaborazione tramite tocco in classi come TouchManipulationBitmap e TouchManipulationManager è la possibilità di riutilizzare queste classi in un programma che consente all'utente di modificare più bitmap.

La pagina Visualizzazione a dispersione bitmap mostra come viene eseguita questa operazione. Anziché definire un campo di tipo TouchManipulationBitmap, la BitmapScatterPage classe definisce un List oggetto di oggetti bitmap:

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;
                }
            }
        }
    }
    ...
}

Il costruttore carica tutte le bitmap disponibili come risorse incorporate e le aggiunge a bitmapCollection. Si noti che la Matrix proprietà viene inizializzata in ogni TouchManipulationBitmap oggetto, pertanto gli angoli superiori sinistro di ogni bitmap sono sfalsati di 100 pixel.

La BitmapScatterView pagina deve anche gestire gli eventi di tocco per più bitmap. Invece di definire un List di ID tocco di oggetti attualmente modificati TouchManipulationBitmap , questo programma richiede un dizionario:

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;
        }
    }
    ...
}

Si noti che la Pressed logica scorre l'oggetto bitmapCollection inverso. Le bitmap spesso si sovrappongono tra loro. Le bitmap più avanti nella raccolta si trovano visivamente sopra le bitmap in precedenza nella raccolta. Se ci sono più bitmap sotto il dito che preme sullo schermo, la parte superiore deve essere quella manipolata da quel dito.

Si noti anche che la logica sposta la Pressed bitmap alla fine della raccolta in modo che si sposta visivamente nella parte superiore della pila di altre bitmap.

Moved Negli eventi e Released il TouchAction gestore chiama il ProcessingTouchEvent metodo in TouchManipulationBitmap proprio come nel programma precedente.

Infine, il PaintSurface gestore chiama il Paint metodo di ogni TouchManipulationBitmap oggetto:

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);
        }
    }
}

Il codice scorre la raccolta e visualizza la pila di bitmap dall'inizio della raccolta alla fine:

Triple screenshot of the Bitmap Scatter View page

Ridimensionamento con un solo dito

Un'operazione di ridimensionamento richiede in genere un movimento di avvicinamento delle dita usando due dita. Tuttavia, è possibile implementare il ridimensionamento con un solo dito facendo spostare gli angoli di una bitmap.

Questa operazione è illustrata nella pagina Scala ad angolo con dita singola. Poiché questo esempio usa un tipo di ridimensionamento leggermente diverso rispetto a quello implementato nella TouchManipulationManager classe , non usa tale classe o la TouchManipulationBitmap classe . Invece, tutta la logica di tocco si trova nel file code-behind. Questa è una logica piuttosto più semplice del solito perché tiene traccia di un solo dito alla volta e ignora semplicemente le dita secondarie che potrebbero toccare lo schermo.

La pagina SingleFingerCornerScale.xaml crea un'istanza della SKCanvasView classe e crea un TouchEffect oggetto per tenere traccia degli eventi di tocco:

<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>

Il file SingleFingerCornerScalePage.xaml.cs carica una risorsa bitmap dalla directory Media e la visualizza usando un SKMatrix oggetto definito come campo:

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);
    }
    ···
}

Questo SKMatrix oggetto viene modificato dalla logica di tocco illustrata di seguito.

Il resto del file code-behind è il TouchEffect gestore eventi. Inizia convertendo la posizione corrente del dito in un SKPoint valore. Per il Pressed tipo di azione, il gestore verifica che nessun altro dito stia toccando lo schermo e che il dito si trova all'interno dei limiti della bitmap.

La parte cruciale del codice è un'istruzione if che coinvolge due chiamate al Math.Pow metodo . Questa matematica controlla se la posizione del dito si trova all'esterno di un'ellisse che riempie la bitmap. In tal caso, si tratta di un'operazione di ridimensionamento. Il dito si trova vicino a uno degli angoli della bitmap e viene determinato un punto pivot che è l'angolo opposto. Se il dito si trova all'interno di questa ellisse, si tratta di un'operazione di panoramica regolare:

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;
        }
    }
}

Il Moved tipo di azione calcola una matrice corrispondente all'attività di tocco dal momento in cui il dito ha premuto lo schermo fino a questo momento. Concatena tale matrice con la matrice in vigore al momento in cui il dito ha premuto per la prima volta la bitmap. L'operazione di ridimensionamento è sempre relativa all'angolo opposto a quello toccato dal dito.

Per bitmap piccole o oblong, un'ellisse interna potrebbe occupare la maggior parte della bitmap e lasciare piccole aree agli angoli per ridimensionare la bitmap. È possibile preferire un approccio leggermente diverso, nel qual caso è possibile sostituire l'intero if blocco impostato isScaling su true con questo codice:

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);
    }
}

Questo codice divide in modo efficace l'area della bitmap in una forma di rombo interno e quattro triangoli agli angoli. Ciò consente ad aree molto più grandi negli angoli di afferrare e ridimensionare la bitmap.