Manipulationen durch Toucheingaben

Verwenden von Matrixtransformationen zum Implementieren von Fingereingaben, Zusammendrücken und Drehungen

In Multitouchumgebungen wie z. B. auf mobilen Geräten verwenden Benutzer häufig ihre Finger, um Objekte auf dem Bildschirm zu bearbeiten. Häufige Gesten wie ein Fingerziehvorgang und ein Zusammendrücken mit zwei Fingern können Objekte verschieben und skalieren oder sogar drehen. Diese Gesten werden in der Regel mithilfe von Transformationsmatrizen implementiert, und in diesem Artikel wird erläutert, wie Sie dies tun.

Eine Bitmap, die übersetzungs-, skalierungs- und drehungsbetrefft ist

Alle hier gezeigten Beispiele verwenden den Xamarin.Forms Touchverfolgungseffekt, der im Artikel "Ereignisse aus Effekten aufrufen" dargestellt wird.

Ziehen und Übersetzen

Eine der wichtigsten Anwendungen von Matrixtransformationen ist die Touchverarbeitung. Ein einzelner SKMatrix Wert kann eine Reihe von Fingereingabevorgängen konsolidieren.

Beim Ziehen mit einem Finger führt der SKMatrix Wert übersetzungen aus. Dies wird auf der Bitmap-Ziehseite veranschaulicht. Die XAML-Datei instanziiert eine SKCanvasView in einer Xamarin.FormsGrid. Der Auflistung dieses GridObjekts wurde ein TouchEffect Objekt hinzugefügtEffects:

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

Theoretisch könnte das TouchEffect Objekt direkt zur Effects Auflistung der SKCanvasView, aber nicht auf allen Plattformen verwendet werden. Da die SKCanvasView Gleiche Größe wie die Grid in dieser Konfiguration ist, fügen Sie sie ebenfalls an die Grid Funktionsweise an.

Die CodeBehind-Datei wird in einer Bitmapressource im Konstruktor geladen und im PaintSurface Handler angezeigt:

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

Ohne weiteren Code ist der SKMatrix Wert immer die Identifikationsmatrix, und er hat keine Auswirkungen auf die Anzeige der Bitmap. Das Ziel des Handlers, der OnTouchEffectAction in der XAML-Datei festgelegt ist, besteht darin, den Matrixwert so zu ändern, dass er Die Fingereingabemanipulationen widerspiegelt.

Der OnTouchEffectAction Handler beginnt mit der Konvertierung des Xamarin.FormsPoint Werts in einen SkiaSharp-Wert SKPoint . Dies ist eine einfache Angelegenheit der Skalierung basierend auf den Width Und Eigenschaften von SKCanvasView (die geräteunabhängige Einheiten sind) und der CanvasSize Eigenschaft, die sich in Einheiten von Height Pixeln befindet:

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

Wenn ein Finger zuerst den Bildschirm berührt, wird ein Ereignis vom Typ TouchActionType.Pressed ausgelöst. Die erste Aufgabe besteht darin, zu bestimmen, ob der Finger die Bitmap berührt. Eine solche Aufgabe wird häufig als Treffertests bezeichnet. In diesem Fall können Treffertests durchgeführt werden, indem ein SKRect Wert erstellt wird, der der Bitmap entspricht, die Matrixtransformation auf MapRectsie angewendet und dann ermittelt wird, ob sich der Touchpunkt innerhalb des transformierten Rechtecks befindet.

Wenn dies der Fall ist, wird das touchId Feld auf die Touch-ID festgelegt, und die Fingerposition wird gespeichert.

Für das TouchActionType.Moved Ereignis werden die Übersetzungsfaktoren des SKMatrix Werts basierend auf der aktuellen Position des Fingers und der neuen Position des Fingers angepasst. Diese neue Position wird zum nächsten Mal bis zum nächsten Mal gespeichert, und die SKCanvasView Position wird ungültig.

Beachten Sie beim Experimentieren mit diesem Programm, dass Sie die Bitmap nur ziehen können, wenn der Finger einen Bereich berührt, in dem die Bitmap angezeigt wird. Obwohl diese Einschränkung für dieses Programm nicht sehr wichtig ist, wird es beim Bearbeiten mehrerer Bitmaps entscheidend.

Zusammendrücken und Skalieren

Was soll passieren, wenn zwei Finger die Bitmap berühren? Wenn sich die beiden Finger parallel bewegen, soll die Bitmap wahrscheinlich mit den Fingern verschoben werden. Wenn die beiden Finger einen Zusammendrücken oder Streckenvorgang ausführen, möchten Sie möglicherweise, dass die Bitmap gedreht (im nächsten Abschnitt erläutert) oder skaliert werden soll. Beim Skalieren einer Bitmap ist es am sinnvollsten, dass die beiden Finger in den gleichen Positionen relativ zur Bitmap wieder Standard und damit die Bitmap entsprechend skaliert werden kann.

Das Behandeln von zwei Fingern auf einmal scheint kompliziert, aber denken Sie daran, dass der TouchAction Handler nur Jeweils Informationen über einen Finger empfängt. Wenn zwei Finger die Bitmap bearbeiten, hat sich für jedes Ereignis die Position eines Fingers geändert, aber das andere hat sich nicht geändert. Im code der Bitmapskalierungsseite unten wird der Finger, der die Position nicht geändert hat, als Pivotpunkt bezeichnet, da die Transformation relativ zu diesem Punkt ist.

Ein Unterschied zwischen diesem Programm und dem vorherigen Programm besteht darin, dass mehrere Touch-IDs gespeichert werden müssen. Ein Wörterbuch wird zu diesem Zweck verwendet, wobei die Touch-ID die Wörterbuchtaste ist und der Wörterbuchwert die aktuelle Position dieses Fingers ist:

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

Die Behandlung der Pressed Aktion ist fast identisch mit dem vorherigen Programm, mit der Ausnahme, dass die ID und der Touchpunkt dem Wörterbuch hinzugefügt werden. Die Released Und Cancelled Aktionen entfernen den Wörterbucheintrag.

Die Behandlung der Moved Aktion ist jedoch komplexer. Wenn nur ein Finger beteiligt ist, ist die Verarbeitung sehr ähnlich wie das vorherige Programm. Bei zwei oder mehr Fingern muss das Programm auch Informationen aus dem Wörterbuch mit dem Finger abrufen, der nicht bewegt wird. Dazu kopieren Sie die Wörterbuchschlüssel in ein Array, und vergleichen Sie dann den ersten Schlüssel mit der ID des Fingers, der verschoben wird. Dadurch kann das Programm den Pivotpunkt abrufen, der dem Finger entspricht, der nicht bewegt wird.

Als Nächstes berechnet das Programm zwei Vektoren der neuen Fingerposition relativ zum Pivotpunkt und die alte Fingerposition relativ zum Pivotpunkt. Die Verhältnisse dieser Vektoren sind Skalierungsfaktoren. Da die Division durch Null eine Möglichkeit ist, müssen diese auf unendliche Werte oder NaN -Werte (keine Zahl) überprüft werden. Wenn alles gut ist, wird eine Skalierungstransformation mit dem SKMatrix als Feld gespeicherten Wert verkettet.

Während Sie mit dieser Seite experimentieren, werden Sie feststellen, dass Sie die Bitmap mit einem oder zwei Fingern ziehen oder mit zwei Fingern skalieren können. Die Skalierung ist anisotrop, was bedeutet, dass die Skalierung in horizontaler und vertikaler Richtung unterschiedlich sein kann. Dadurch wird das Seitenverhältnis verzerrt, sie können aber auch die Bitmap kippen, um ein Spiegel Bild zu erstellen. Möglicherweise stellen Sie auch fest, dass Sie die Bitmap auf eine Nulldimension verkleinern können, und sie verschwindet. Im Produktionscode sollten Sie sich davor schützen.

Drehung mit zwei Fingern

Mit der Bitmapdrehungsseite können Sie zwei Finger für die Drehung oder isotrope Skalierung verwenden. Die Bitmap behält immer das richtige Seitenverhältnis bei. Die Verwendung von zwei Fingern für die Drehung und die anisotrope Skalierung funktioniert nicht sehr gut, da die Bewegung der Finger für beide Aufgaben sehr ähnlich ist.

Der erste große Unterschied in diesem Programm ist die Treffertestlogik. Die vorherigen Programme haben die Contains Methode SKRect verwendet, um zu bestimmen, ob sich der Touchpunkt innerhalb des transformierten Rechtecks befindet, das der Bitmap entspricht. Während der Benutzer die Bitmap bearbeitet, wird die Bitmap möglicherweise gedreht und SKRect kann kein gedrehtes Rechteck richtig darstellen. Sie könnten befürchten, dass die Treffertestlogik in diesem Fall ziemlich komplexe Analysegeometrie implementieren muss.

Es ist jedoch eine Verknüpfung verfügbar: Ermitteln, ob ein Punkt innerhalb der Grenzen eines transformierten Rechtecks liegt, ist identisch mit der Bestimmung, ob ein umgekehrter transformierter Punkt innerhalb der Grenzen des untransformierten Rechtecks liegt. Dies ist eine viel einfachere Berechnung, und die Logik kann weiterhin die bequeme Contains Methode verwenden:

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

Die Logik für das Moved Ereignis beginnt wie das vorherige Programm. Zwei benannte oldVector Vektoren und newVector werden basierend auf dem vorherigen und dem aktuellen Punkt des bewegenden Fingers und dem Pivotpunkt des ungeovten Fingers berechnet. Dann werden jedoch Winkel dieser Vektoren bestimmt, und der Unterschied ist der Drehwinkel.

Die Skalierung kann auch beteiligt sein, sodass der alte Vektor basierend auf dem Drehwinkel gedreht wird. Die relative Größe der beiden Vektoren ist nun der Skalierungsfaktor. Beachten Sie, dass derselbe scale Wert für die horizontale und vertikale Skalierung verwendet wird, sodass die Skalierung isotropisch ist. Das matrix Feld wird sowohl durch die Drehungsmatrix als auch durch eine Skalierungsmatrix angepasst.

Wenn Ihre Anwendung die Touchverarbeitung für eine einzelne Bitmap (oder ein anderes Objekt) implementieren muss, können Sie den Code aus diesen drei Beispielen für Ihre eigene Anwendung anpassen. Wenn Sie jedoch die Touchverarbeitung für mehrere Bitmaps implementieren müssen, sollten Sie diese Fingereingabevorgänge wahrscheinlich in anderen Klassen kapseln.

Kapselung der Toucheingabevorgänge

Auf der Seite "Touchbearbeitung " wird die Fingereingabebearbeitung einer einzelnen Bitmap veranschaulicht, wobei jedoch mehrere andere Dateien verwendet werden, die einen Großteil der oben gezeigten Logik kapseln. Die erste dieser Dateien ist die TouchManipulationMode Aufzählung, die die verschiedenen Arten der Touchbearbeitung angibt, die vom Code implementiert werden, den Sie sehen werden:

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

PanOnly ist ein Ziehvorgang mit einem Finger, der mit Übersetzung implementiert wird. Alle nachfolgenden Optionen umfassen auch die Verschiebung, umfassen jedoch zwei Finger: IsotropicScale Eine Zusammendrückoperation, die zu einer gleichmäßigen Skalierung des Objekts in horizontaler und vertikaler Richtung führt. AnisotropicScale ermöglicht eine ungleiche Skalierung.

Die ScaleRotate Option ist für die Skalierung und Drehung mit zwei Fingern. Die Skalierung ist isotrop. Wie zuvor Erwähnung, ist die Implementierung der Zweifingerdrehung mit anisotroper Skalierung problematisch, da die Fingerbewegungen im Wesentlichen gleich sind.

Die ScaleDualRotate Option fügt eine Drehung mit einem Finger hinzu. Wenn ein einzelner Finger das Objekt zieht, wird das gezogene Objekt zuerst um seine Mitte gedreht, sodass sich die Mitte des Objekts mit dem Ziehvektor nach oben richtet.

Die Datei "TouchManipulationPage.xaml" enthält eine Picker mit den Membern der TouchManipulationMode Enumeration:

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

In Richtung unten befindet sich eine SKCanvasView und eine TouchEffect an die einzelne Zelle angefügte Zelle Grid , die sie umschließt.

Die TouchManipulationPage.xaml.cs CodeBehind-Datei weist ein bitmap Feld auf, ist jedoch nicht vom Typ SKBitmap. Der Typ ist TouchManipulationBitmap (ein Kurs, den Sie in Kürze sehen):

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

Der Konstruktor instanziiert ein TouchManipulationBitmap Objekt und übergibt an den Konstruktor eine SKBitmap aus einer eingebetteten Ressource abgerufene Ressource. Der Konstruktor endet, indem die Mode Eigenschaft der TouchManager Eigenschaft des TouchManipulationBitmap Objekts auf ein Element der TouchManipulationMode Enumeration festgelegt wird.

Der SelectedIndexChanged Handler für die Picker Eigenschaft legt Mode diese Eigenschaft fest:

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

Der TouchAction Handler der TouchEffect instanziierten XAML-Datei ruft zwei Methoden in TouchManipulationBitmap benannter HitTest Und 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;
        }
    }
    ...
}

Wenn die HitTest Methode zurückgegeben wird true , d. h., ein Finger hat den Bildschirm innerhalb des Bereichs berührt, der von der Bitmap belegt wird, wird die Touch-ID der TouchIds Auflistung hinzugefügt. Diese ID stellt die Abfolge von Touchereignissen für diesen Finger dar, bis der Finger vom Bildschirm aufhebt. Wenn mehrere Finger die Bitmap berühren, enthält die touchIds Auflistung eine Touch-ID für jeden Finger.

Der TouchAction Handler ruft auch die ProcessTouchEvent Klasse in TouchManipulationBitmap. Dies ist der Ort, an dem einige (aber nicht alle) der echten Fingereingabeverarbeitung auftreten.

Die TouchManipulationBitmap Klasse ist eine Wrapperklasse, die SKBitmap Code zum Rendern der Bitmap- und Verarbeitungsereignisse enthält. Es funktioniert in Verbindung mit allgemeinerem Code in einer TouchManipulationManager Klasse (die Sie in Kürze sehen).

Der TouchManipulationBitmap Konstruktor speichert und SKBitmap instanziiert zwei Eigenschaften, die TouchManager Eigenschaft vom Typ TouchManipulationManager und die Matrix Eigenschaft des Typs 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; }
    ...
}

Diese Matrix Eigenschaft ist die angesammelte Transformation, die sich aus allen Touchaktivitäten ergibt. Wie Sie sehen, wird jedes Touchereignis in eine Matrix aufgelöst, die dann mit dem SKMatrix von der Matrix Eigenschaft gespeicherten Wert verkettet wird.

Das TouchManipulationBitmap Objekt zeichnet sich selbst in seiner Paint Methode. Das Argument ist ein SKCanvas Objekt. Dies SKCanvas kann bereits eine Transformation darauf angewendet haben, sodass die Methode die Paint mit der Matrix Bitmap verknüpfte Eigenschaft mit der vorhandenen Transformation verkettet und den Zeichenbereich wiederhergestellt, wenn sie fertig ist:

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

Die HitTest Methode gibt zurück true , wenn der Benutzer den Bildschirm an einem Punkt innerhalb der Begrenzungen der Bitmap berührt. Dies verwendet die Logik, die weiter oben auf der Bitmapdrehungsseite angezeigt wird:

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

Die zweite öffentliche Methode in TouchManipulationBitmap ist ProcessTouchEvent. Wenn diese Methode aufgerufen wird, wurde bereits festgestellt, dass das Touchereignis zu dieser bestimmten Bitmap gehört. Die Methode Standard enthält ein Wörterbuch von TouchManipulationInfo Objekten, das einfach der vorherige Punkt und der neue Punkt jedes Fingers ist:

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

    public SKPoint NewPoint { set; get; }
}

Hier sehen Sie das Wörterbuch und die ProcessTouchEvent Methode selbst:

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

In den Moved Und Released Ereignissen ruft die Methode auf Manipulate. Zu diesen Zeiten enthält das touchDictionary Objekt ein oder mehrere TouchManipulationInfo Objekte. Wenn das touchDictionary Element ein Element enthält, ist es wahrscheinlich, dass die PreviousPoint Werte NewPoint ungleich sind und die Bewegung eines Fingers darstellen. Wenn mehrere Finger die Bitmap berühren, enthält das Wörterbuch mehrere Elemente, aber nur eines dieser Elemente weist unterschiedliche PreviousPoint Werte auf NewPoint . Der rest hat gleich PreviousPoint und NewPoint werte.

Dies ist wichtig: Die Manipulate Methode kann davon ausgehen, dass sie die Bewegung nur mit einem Finger verarbeitet. Zum Zeitpunkt dieses Aufrufs bewegen sich keine der anderen Finger, und wenn sie sich wirklich bewegen (wie wahrscheinlich), werden diese Bewegungen in zukünftigen Aufrufen Manipulateverarbeitet.

Die Manipulate Methode kopiert das Wörterbuch zunächst aus Gründen der Einfachheit in ein Array. Es ignoriert alles andere als die ersten beiden Einträge. Wenn mehr als zwei Finger versuchen, die Bitmap zu bearbeiten, werden die anderen ignoriert. Manipulate ist das letzte Mitglied von 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;
    }
}

Wenn ein Finger die Bitmap bearbeitet, Manipulate wird die OneFingerManipulate Methode des TouchManipulationManager Objekts aufgerufen. Für zwei Finger ruft TwoFingerManipulatees . Die Argumente für diese Methoden sind identisch: Die prevPoint Argumente und newPoint Argumente stellen den Finger dar, der sich bewegt. Das pivotPoint Argument unterscheidet sich jedoch für die beiden Aufrufe:

Bei der Manipulation mit einem Finger ist die pivotPoint Mitte der Bitmap. Dies ist die Möglichkeit der Drehung mit einem Finger. Bei der Manipulation mit zwei Fingern gibt das Ereignis die Bewegung nur eines Fingers an, sodass es sich pivotPoint um den Finger handelt, der nicht bewegt wird.

In beiden Fällen TouchManipulationManager wird ein SKMatrix Wert zurückgegeben, der von der Methode mit der aktuellen Matrix Eigenschaft verkettet wird, die TouchManipulationPage zum Rendern der Bitmap verwendet wird.

TouchManipulationManager ist generalisiert und verwendet keine anderen Dateien außer TouchManipulationMode. Möglicherweise können Sie diese Klasse ohne Änderung in Ihren eigenen Anwendungen verwenden. Sie definiert eine einzelne Eigenschaft vom Typ TouchManipulationMode:

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

Sie sollten die AnisotropicScale Option jedoch wahrscheinlich vermeiden. Mit dieser Option ist es sehr einfach, die Bitmap zu bearbeiten, sodass einer der Skalierungsfaktoren null wird. Dadurch wird die Bitmap nicht mehr sichtbar, und sie kann niemals zurückgegeben werden. Wenn Sie wirklich anisotrope Skalierung benötigen, sollten Sie die Logik verbessern, um unerwünschte Ergebnisse zu vermeiden.

TouchManipulationManager verwendet Vektoren, aber da es keine SKVector Struktur in SkiaSharp gibt, SKPoint wird stattdessen verwendet. SKPoint unterstützt den Subtraktionsoperator, und das Ergebnis kann als Vektor behandelt werden. Die einzige vektorspezifische Logik, die hinzugefügt werden muss, ist eine Magnitude Berechnung:

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

Wenn die Drehung ausgewählt wurde, behandeln die Manipulationsmethoden mit einem Finger und zwei Fingern zuerst die Drehung. Wenn eine Drehung erkannt wird, wird die Drehungskomponente effektiv entfernt. Was neu Standard wird als Verschiebung und Skalierung interpretiert.

Dies ist die OneFingerManipulate Methode. Wenn die Drehung mit einem Finger nicht aktiviert wurde, ist die Logik einfach – sie verwendet einfach den vorherigen Punkt und neuen Punkt, um einen Vektor delta zu erstellen, der genau der Übersetzung entspricht. Wenn die Drehung mit einem Finger aktiviert ist, verwendet die Methode Winkel vom Pivotpunkt (die Mitte der Bitmap) zum vorherigen Punkt und neuen Punkt, um eine Drehungsmatrix zu erstellen:

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

In der TwoFingerManipulate Methode ist der Pivotpunkt die Position des Fingers, die sich in diesem bestimmten Touchereignis nicht bewegt. Die Drehung ist der Drehung mit einem Finger sehr ähnlich, und dann wird der benannte oldVector Vektor (basierend auf dem vorherigen Punkt) für die Drehung angepasst. Die Um Standard bewegung wird als Skalierung interpretiert:

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

Sie werden feststellen, dass in dieser Methode keine explizite Übersetzung vorhanden ist. Sowohl die Methoden als MakeScale auch die MakeRotation Methoden basieren jedoch auf dem Pivotpunkt und enthalten implizite Übersetzungen. Wenn Sie zwei Finger auf der Bitmap verwenden und in die gleiche Richtung ziehen, TouchManipulation wird eine Reihe von Touchereignissen zwischen den beiden Fingern wechselt. Wenn sich jeder Finger relativ zum anderen bewegt, führt die Skalierung oder Drehung zu den Ergebnissen, wird jedoch von der Bewegung des anderen Fingers negiert, und das Ergebnis wird übersetzt.

Der Standard einzige teil der Seite "Touchbearbeitung" ist der PaintSurface Handler in der TouchManipulationPage CodeBehind-Datei. Dadurch wird die Paint Methode des TouchManipulationBitmap, die die Matrix anwendet, die die akkumulierte Touchaktivität darstellt:

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

Der PaintSurface Handler wird beendet, indem ein MatrixDisplay Objekt mit der angesammelten Touchmatrix angezeigt wird:

Dreifacher Screenshot der Seite

Bearbeiten mehrerer Bitmaps

Einer der Vorteile beim Isolieren von Touchverarbeitungscode in Klassen wie TouchManipulationBitmap und TouchManipulationManager die Möglichkeit, diese Klassen in einem Programm wiederzuverwenden, mit dem der Benutzer mehrere Bitmaps bearbeiten kann.

Auf der Seite "Bitmap-Punktansicht " wird gezeigt, wie dies erfolgt. Anstatt ein Feld vom Typ TouchManipulationBitmapzu definieren, definiert die BitmapScatterPage Klasse ein List Bitmapobjekt:

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

Der Konstruktor wird in allen Bitmaps geladen, die als eingebettete Ressourcen verfügbar sind, und fügt sie der bitmapCollectionDatei hinzu. Beachten Sie, dass die Matrix Eigenschaft für jedes TouchManipulationBitmap Objekt initialisiert wird, sodass die oberen linken Ecken jeder Bitmap um 100 Pixel versetzt werden.

Die BitmapScatterView Seite muss auch Touchereignisse für mehrere Bitmaps behandeln. Anstatt eine List Touch-IDs von aktuell bearbeiteten TouchManipulationBitmap Objekten zu definieren, erfordert dieses Programm ein Wörterbuch:

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

Beachten Sie, wie die Pressed Logik umgekehrt durchläuft bitmapCollection . Die Bitmaps überlappen sich häufig gegenseitig. Die Bitmaps weiter unten in der Auflistung befinden sich visuell über den Bitmaps weiter oben in der Auflistung. Wenn mehrere Bitmaps unter dem Finger vorhanden sind, die auf dem Bildschirm gedrückt werden, muss die oberste Bitmap die von diesem Finger bearbeitete sein.

Beachten Sie außerdem, dass die Pressed Logik diese Bitmap an das Ende der Auflistung verschiebt, sodass sie visuell an den Anfang des Stapels anderer Bitmaps verschoben wird.

In den Moved Und-Ereignissen Released ruft der TouchAction Handler die ProcessingTouchEvent Methode TouchManipulationBitmap genau wie das frühere Programm auf.

Schließlich ruft der PaintSurface Handler die Paint Methode der einzelnen TouchManipulationBitmap Objekte auf:

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

Der Code durchläuft die Auflistung und zeigt den Stapel von Bitmaps vom Anfang der Auflistung bis zum Ende an:

Dreifacher Screenshot der Seite

Skalierung mit einem Finger

Bei einem Skalierungsvorgang ist in der Regel eine Zusammendrückbewegung mit zwei Fingern erforderlich. Es ist jedoch möglich, die Skalierung mit einem einzelnen Finger zu implementieren, indem der Finger die Ecken einer Bitmap bewegt.

Dies wird auf der Seite "Single Finger Corner Scale " veranschaulicht. Da in diesem Beispiel eine etwas andere Art von Skalierung verwendet wird als die in der TouchManipulationManager Klasse implementierte, wird diese Klasse oder die TouchManipulationBitmap Klasse nicht verwendet. Stattdessen befindet sich die gesamte Touchlogik in der CodeBehind-Datei. Dies ist etwas einfachere Logik als üblich, da sie nur einen Finger gleichzeitig verfolgt und einfach alle sekundären Finger ignoriert, die möglicherweise den Bildschirm berühren.

Die SingleFingerCornerScale.xaml-Seite instanziiert die SKCanvasView Klasse und erstellt ein TouchEffect Objekt zum Nachverfolgen von Touchereignissen:

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

Die SingleFingerCornerScalePage.xaml.cs Datei lädt eine Bitmapressource aus dem Medienverzeichnis und zeigt sie mithilfe eines SKMatrix Objekts an, das als Feld definiert ist:

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

Dieses SKMatrix Objekt wird von der unten gezeigten Touchlogik geändert.

Der re Standard der der CodeBehind-Datei ist der TouchEffect Ereignishandler. Zunächst wird die aktuelle Position des Fingers in einen SKPoint Wert konvertiert. Für den Pressed Aktionstyp überprüft der Handler, ob kein anderer Finger den Bildschirm berührt, und dass sich der Finger innerhalb der Grenzen der Bitmap befindet.

Der entscheidende Teil des Codes ist eine if Anweisung mit zwei Aufrufen der Math.Pow Methode. Diese Mathematik überprüft, ob sich die Fingerposition außerhalb einer Ellipse befindet, die die Bitmap ausfüllt. Wenn ja, ist dies ein Skalierungsvorgang. Der Finger befindet sich in der Nähe einer der Ecken der Bitmap, und ein Pivotpunkt wird bestimmt, dass es sich um die gegenüberliegende Ecke handelt. Wenn sich der Finger innerhalb dieser Ellipse befindet, handelt es sich um einen normalen Verschiebungsvorgang:

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

Der Moved Aktionstyp berechnet eine Matrix, die der Touchaktivität entspricht, ab dem Zeitpunkt, zu dem der Finger den Bildschirm bis zu diesem Zeitpunkt gedrückt hat. Sie verkettet diese Matrix mit der tatsächlichen Matrix, wenn der Finger zuerst die Bitmap gedrückt hat. Der Skalierungsvorgang ist immer relativ zur Ecke gegenüber dem, den der Finger berührt hat.

Bei kleinen oder länglichen Bitmaps belegt eine innere Ellipse möglicherweise den größten Teil der Bitmap und lassen winzige Bereiche an den Ecken, um die Bitmap zu skalieren. Möglicherweise bevorzugen Sie einen etwas anderen Ansatz, in diesem Fall können Sie diesen gesamten if Block ersetzen, der durch diesen Code festgelegt wird isScalingtrue :

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

Dieser Code teilt den Bereich der Bitmap effektiv in eine innere Rautenform und vier Dreiecke an den Ecken auf. Dies ermöglicht es viel größeren Bereichen an den Ecken, die Bitmap zu erfassen und zu skalieren.