Udostępnij za pośrednictwem


Manipulacje za pomocą dotyku

Używanie przekształceń macierzy do implementowania przeciągania dotykowego, szczypania i obrotu

W środowiskach wielodotyku, takich jak te na urządzeniach przenośnych, użytkownicy często używają palców do manipulowania obiektami na ekranie. Typowe gesty, takie jak przeciąganie jednym palcem i szczypta dwu palcami, mogą przenosić i skalować obiekty, a nawet obracać je. Te gesty są zwykle implementowane przy użyciu macierzy transformacji, a w tym artykule pokazano, jak to zrobić.

Mapa bitowa poddana translacji, skalowaniu i rotacji

Wszystkie pokazane tutaj przykłady używają efektu śledzenia dotykowego Xamarin.Forms przedstawionego w artykule Wywoływanie zdarzeń z efektów.

Przeciąganie i tłumaczenie

Jednym z najważniejszych zastosowań transformacji macierzy jest przetwarzanie dotykowe. Pojedyncza SKMatrix wartość może skonsolidować serię operacji dotykowych.

W przypadku przeciągania SKMatrix pojedynczego palca wartość wykonuje tłumaczenie. Jest to pokazane na stronie Przeciąganie mapy bitowej . Plik XAML tworzy wystąpienie elementu SKCanvasView w pliku Xamarin.FormsGrid. Obiekt TouchEffect został dodany do Effects kolekcji tego Gridelementu :

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

Teoretycznie TouchEffect obiekt można dodać bezpośrednio do Effects kolekcji SKCanvasViewobiektu , ale nie działa na wszystkich platformach. Ponieważ parametr SKCanvasView ma taki sam rozmiar jak Grid w tej konfiguracji, dołączanie go do Grid elementu działa tak samo dobrze.

Plik związany z kodem ładuje się w zasobie mapy bitowej w konstruktorze i wyświetla go w procedurze PaintSurface obsługi:

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

    public BitmapDraggingPage()
    {
        InitializeComponent();

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

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

        canvas.Clear();

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

Bez żadnego dalszego kodu SKMatrix wartość jest zawsze macierzą identyfikacji i nie będzie miała wpływu na wyświetlanie mapy bitowej. Celem OnTouchEffectAction programu obsługi ustawionego w pliku XAML jest zmiana wartości macierzy w celu odzwierciedlenia manipulacji dotykiem.

Procedura OnTouchEffectAction obsługi rozpoczyna się od przekonwertowania Xamarin.FormsPoint wartości na wartość SkiaSharp SKPoint . Jest to prosta kwestia skalowania na Width podstawie właściwości SKCanvasView i Height (które są jednostkami niezależnymi od urządzenia) i CanvasSize właściwością, która znajduje się w jednostkach pikseli:

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

Gdy palec po raz pierwszy dotyka ekranu, zdarzenie typu TouchActionType.Pressed jest uruchamiane. Pierwszym zadaniem jest ustalenie, czy palec dotyka mapy bitowej. Takie zadanie jest często nazywane testowaniem trafień. W takim przypadku można przeprowadzić testowanie trafień przez utworzenie SKRect wartości odpowiadającej mapie bitowej, zastosowanie do niej MapRectprzekształcenia macierzy za pomocą elementu , a następnie określenie, czy punkt dotykowy znajduje się wewnątrz przekształconego prostokąta.

Jeśli tak jest, touchId to pole jest ustawione na identyfikator dotykowy, a pozycja palca zostanie zapisana.

TouchActionType.Moved W przypadku zdarzenia czynniki SKMatrix tłumaczenia wartości są dostosowywane na podstawie bieżącej pozycji palca i nowej pozycji palca. Ta nowa pozycja zostanie zapisana po raz kolejny i SKCanvasView unieważniona.

Podczas eksperymentu z tym programem zwróć uwagę, że możesz przeciągać mapę bitową tylko wtedy, gdy palec dotyka obszaru, w którym jest wyświetlana mapa bitowa. Chociaż to ograniczenie nie jest bardzo ważne dla tego programu, staje się kluczowe podczas manipulowania wieloma mapami bitowymi.

Szczypanie i skalowanie

Co chcesz zrobić, gdy dwa palce dotykają mapy bitowej? Jeśli dwa palce poruszają się równolegle, prawdopodobnie chcesz, aby mapa bitowa poruszała się wraz z palcami. Jeśli dwa palce wykonują operację szczyptowania lub rozciągania, możesz chcieć, aby mapa bitowa została obrócona (omówiono w następnej sekcji) lub przeskalowana. Podczas skalowania mapy bitowej najlepiej jest zachować dwa palce w tych samych pozycjach względem mapy bitowej i odpowiednio skalować mapę bitową.

Obsługa dwóch palców jednocześnie wydaje się skomplikowana, ale należy pamiętać, że TouchAction program obsługi otrzymuje tylko informacje o jednym palcu naraz. Jeśli dwie palce manipulują mapą bitową, to dla każdego zdarzenia jeden palec zmienił położenie, ale drugi nie uległ zmianie. W poniższym kodzie strony Skalowanie map bitowych palec, który nie zmienił położenia, jest nazywany punktem przestawnym, ponieważ przekształcenie jest względem tego punktu.

Jedną z różnic między tym programem a poprzednim programem jest to, że należy zapisać wiele identyfikatorów dotykowych. Słownik jest używany w tym celu, gdzie touch ID jest kluczem słownika, a wartość słownika jest bieżącą pozycją tego palca:

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

Obsługa Pressed akcji jest prawie taka sama jak poprzedni program, z tą różnicą, że identyfikator i punkt dotykowy są dodawane do słownika. Akcje Released i Cancelled usuwają wpis słownika.

Obsługa Moved akcji jest jednak bardziej skomplikowana. Jeśli jest tylko jeden palec, przetwarzanie jest bardzo takie samo jak w poprzednim programie. W przypadku co najmniej dwóch palców program musi również uzyskać informacje ze słownika obejmującego palec, który nie porusza się. Robi to przez skopiowanie kluczy słownika do tablicy, a następnie porównanie pierwszego klucza z identyfikatorem przenoszonego palca. Dzięki temu program może uzyskać punkt przestawny odpowiadający palcem, który nie porusza się.

Następnie program oblicza dwa wektory położenia nowego palca względem punktu przestawnego i starego położenia palca względem punktu przestawnego. Współczynniki tych wektorów są czynnikami skalowania. Ponieważ dzielenie według zera jest możliwe, muszą być sprawdzane pod kątem wartości nieskończonych lub wartości NaN (a nie liczby). Jeśli wszystko jest dobrze, przekształcenie skalowania jest łączone z wartością SKMatrix zapisaną jako pole.

Podczas eksperymentu z tą stroną zauważysz, że możesz przeciągnąć mapę bitową z jednym lub dwoma palcami lub przeskalować ją dwoma palcami. Skalowanie jest anisotropowe, co oznacza, że skalowanie może być różne w kierunkach poziomych i pionowych. To zniekształca współczynnik proporcji, ale umożliwia również przerzucanie mapy bitowej w celu utworzenia obrazu dublowanego. Możesz również odkryć, że można zmniejszyć mapę bitową do wymiaru zerowego i zniknąć. W kodzie produkcyjnym warto chronić się przed tym.

Obrót dwoma palcami

Strona Obróć mapy bitowej umożliwia użycie dwóch palców do obrotu lub izotropowego skalowania. Mapa bitowa zawsze zachowuje prawidłowy współczynnik proporcji. Użycie dwóch palców do obrotu i anisotropowego skalowania nie działa bardzo dobrze, ponieważ ruch palców jest bardzo podobny dla obu zadań.

Pierwszą dużą różnicą w tym programie jest logika testowania trafień. Poprzednie programy używały Contains metody SKRect określania, czy punkt dotykowy znajduje się w przekształconym prostokątze odpowiadającym mapie bitowej. Jednak gdy użytkownik manipuluje mapą bitową, mapa bitowa może być obracana i SKRect nie może prawidłowo reprezentować obróconego prostokąta. Można się obawiać, że logika testowania trafień musi implementować dość złożoną geometrię analityczną w tym przypadku.

Dostępny jest jednak skrót: Określenie, czy punkt znajduje się w granicach przekształconego prostokąta, jest taki sam jak określenie, czy odwrotny przekształcony punkt znajduje się w granicach nieprzetłumaczonego prostokąta. Jest to znacznie łatwiejsze obliczenie, a logika może nadal korzystać z wygodnej Contains metody:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                break;

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

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

Logika Moved zdarzenia zaczyna się tak jak poprzedni program. Dwa wektory o nazwie oldVector i newVector są obliczane na podstawie poprzedniego i bieżącego punktu ruchomego palca oraz punktu obrotu palca unmoving. Jednak kąty tych wektorów są określane, a różnica jest kątem obrotu.

Skalowanie może być również zaangażowane, więc stary wektor jest obracany na podstawie kąta obrotu. Względna wielkość dwóch wektorów jest teraz czynnikiem skalowania. Zwróć uwagę, że ta sama scale wartość jest używana do skalowania w poziomie i w pionie, aby skalowanie było izotropowe. Pole matrix jest dostosowywane zarówno przez macierz obrotu, jak i macierz skalowania.

Jeśli aplikacja musi zaimplementować przetwarzanie dotykowe dla pojedynczej mapy bitowej (lub innego obiektu), możesz dostosować kod z tych trzech przykładów dla własnej aplikacji. Jeśli jednak musisz zaimplementować przetwarzanie dotykowe dla wielu map bitowych, prawdopodobnie chcesz hermetyzować te operacje dotykowe w innych klasach.

Hermetyzowanie operacji dotykowych

Strona Manipulowanie dotykiem pokazuje manipulowanie dotykiem pojedynczej mapy bitowej, ale przy użyciu kilku innych plików, które hermetyzują znaczną część logiki pokazanej powyżej. Pierwszy z tych plików to TouchManipulationMode wyliczenie, które wskazuje różne typy manipulacji dotykiem zaimplementowane przez kod, który zobaczysz:

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

PanOnly to przeciąganie jednym palcem, które jest implementowane z tłumaczeniem. Wszystkie kolejne opcje obejmują również przesuwanie, ale obejmują dwa palce: IsotropicScale to operacja szczypta, która powoduje równomierne skalowanie obiektu w kierunkach poziomych i pionowych. AnisotropicScale umożliwia nierówne skalowanie.

Opcja ScaleRotate dotyczy skalowania i obrotu dwu palcami. Skalowanie jest izotropiczne. Jak wspomniano wcześniej, implementowanie rotacji dwóch palców za pomocą skalowania anisotropowego jest problematyczne, ponieważ ruchy palców są zasadniczo takie same.

Opcja ScaleDualRotate dodaje obrót jednym palcem. Gdy pojedynczy palec przeciąga obiekt, przeciągnięty obiekt jest najpierw obracany wokół środka, tak aby środek obiektu był wyrównany do wektora przeciągania.

Plik TouchManipulationPage.xaml zawiera element Picker z elementami członkowskimi TouchManipulationMode wyliczenia:

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

W dolnej części znajduje się element i SKCanvasView TouchEffect dołączony do pojedynczej komórki Grid , która ją otacza.

Plik TouchManipulationPage.xaml.cs bitmap zawiera pole, ale nie jest typu .SKBitmap Typ to TouchManipulationBitmap (klasa, którą zobaczysz wkrótce):

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

    public TouchManipulationPage()
    {
        InitializeComponent();

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

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

Konstruktor tworzy wystąpienie TouchManipulationBitmap obiektu, przekazując do konstruktora SKBitmap obiekt uzyskany z zasobu osadzonego. Konstruktor kończy, ustawiając Mode właściwość TouchManager właściwości TouchManipulationBitmap obiektu na element członkowski TouchManipulationMode wyliczenia.

Program SelectedIndexChanged obsługi dla Picker tej właściwości ustawia również następującą Mode właściwość:

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

Procedura TouchAction obsługi TouchEffect wystąpienia w pliku XAML wywołuje dwie metody w TouchManipulationBitmap nazwach HitTest i 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;
        }
    }
    ...
}

HitTest Jeśli metoda zwróci true wartość — co oznacza, że palec dotknął ekranu w obszarze zajmowanym przez mapę bitową , identyfikator dotykowy zostanie dodany do TouchIds kolekcji. Ten identyfikator reprezentuje sekwencję zdarzeń dotyku dla tego palca, aż palec unosi się z ekranu. Jeśli wiele palców dotyka mapy bitowej, touchIds kolekcja zawiera identyfikator dotykowy dla każdego palca.

Procedura TouchAction obsługi wywołuje również klasę ProcessTouchEvent w pliku TouchManipulationBitmap. W tym miejscu dochodzi do niektórych (ale nie wszystkich) rzeczywistego przetwarzania dotykowego.

Klasa TouchManipulationBitmap jest klasą SKBitmap otoki zawierającą kod renderowania mapy bitowej i przetwarzania zdarzeń dotykowych. Działa w połączeniu z bardziej uogólniony kod w TouchManipulationManager klasie (co wkrótce zobaczysz).

Konstruktor TouchManipulationBitmap zapisuje SKBitmap i tworzy wystąpienie dwóch właściwości, TouchManager właściwość typu TouchManipulationManager i Matrix właściwość typu SKMatrix:

class TouchManipulationBitmap
{
    SKBitmap bitmap;
    ...

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

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

    public TouchManipulationManager TouchManager { set; get; }

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

Ta Matrix właściwość jest skumulowaną transformacją wynikającą ze wszystkich działań dotykowych. Jak zobaczysz, każde zdarzenie dotykowe jest rozpoznawane w macierzy, która jest następnie połączona z wartością SKMatrix przechowywaną przez Matrix właściwość.

Obiekt TouchManipulationBitmap rysuje się w swojej Paint metodzie. Argument jest obiektem SKCanvas . Może to SKCanvas już zostać zastosowane przekształcenie, więc Paint metoda łączy Matrix właściwość skojarzona z mapą bitową do istniejącej transformacji i przywraca kanwę po zakończeniu:

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

Metoda HitTest zwraca true wartość, jeśli użytkownik dotyka ekranu w punkcie w granicach mapy bitowej. Używa to logiki pokazanej wcześniej na stronie Rotacja mapy bitowej:

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

Druga publiczna metoda w pliku TouchManipulationBitmap to ProcessTouchEvent. Po wywołaniu tej metody została już ustalona, że zdarzenie dotykowe należy do tej konkretnej mapy bitowej. Metoda obsługuje słownik TouchManipulationInfo obiektów, który jest po prostu poprzednim punktem i nowym punktem każdego palca:

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

    public SKPoint NewPoint { set; get; }
}

Oto słownik i ProcessTouchEvent sama metoda:

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

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

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

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

W zdarzeniach Moved i Released metoda wywołuje metodę Manipulate. W tym czasie obiekt touchDictionary zawiera co najmniej jeden TouchManipulationInfo obiekt. Jeśli element touchDictionary zawiera jeden element, prawdopodobnie PreviousPoint wartości i NewPoint są nierówne i reprezentują ruch palca. Jeśli wiele palców dotyka mapy bitowej, słownik zawiera więcej niż jeden element, ale tylko jeden z tych elementów ma różne PreviousPoint wartości i NewPoint . Wszystkie pozostałe mają równe PreviousPoint wartości i NewPoint .

Jest to ważne: Manipulate Metoda może zakładać, że przetwarza ruch tylko jednego palca. W momencie tego wywołania żaden z innych palców nie porusza się, a jeśli naprawdę porusza się (co jest prawdopodobne), te ruchy zostaną przetworzone w przyszłych wywołaniach do Manipulate.

Metoda Manipulate najpierw kopiuje słownik do tablicy dla wygody. Ignoruje wszystkie inne niż pierwsze dwa wpisy. Jeśli więcej niż dwa palce próbują manipulować mapą bitową, pozostałe są ignorowane. Manipulate jest ostatnim elementem członkowskim programu 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;
    }
}

Jeśli jeden palec manipuluje mapą bitową, Manipulate wywołuje metodę OneFingerManipulate TouchManipulationManager obiektu . W przypadku dwóch palców wywołuje metodę TwoFingerManipulate. Argumenty tych metod są takie same: prevPoint argumenty i newPoint reprezentują palec, który się porusza. pivotPoint Ale argument jest inny dla dwóch wywołań:

W przypadku manipulowania pivotPoint jednym palcem element jest środek mapy bitowej. Ma to na celu obracanie się jednym palcem. W przypadku manipulowania dwoma palcami zdarzenie wskazuje ruch tylko jednego palca, tak aby pivotPoint był palcem, który nie porusza się.

W obu przypadkach TouchManipulationManager zwraca SKMatrix wartość, która metoda łączy się z bieżącą Matrix właściwością TouchManipulationPage używaną do renderowania mapy bitowej.

TouchManipulationManager jest uogólniony i nie używa żadnych innych plików z wyjątkiem TouchManipulationMode. Możesz użyć tej klasy bez konieczności wprowadzania zmian we własnych aplikacjach. Definiuje pojedynczą właściwość typu TouchManipulationMode:

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

Prawdopodobnie jednak chcesz uniknąć AnisotropicScale tej opcji. Dzięki tej opcji bardzo łatwo jest manipulować mapą bitową, aby jeden z czynników skalowania stał się zerowy. To sprawia, że mapa bitowa znika z oczu, nigdy nie wraca. Jeśli naprawdę potrzebujesz skalowania anisotropowego, warto ulepszyć logikę, aby uniknąć niepożądanych wyników.

TouchManipulationManager używa wektorów, ale ponieważ w skiaSharp nie SKVector ma żadnej struktury, SKPoint zamiast tego jest używana. SKPoint obsługuje operator odejmowania, a wynik może być traktowany jako wektor. Jedyną logiką specyficzną Magnitude dla wektorów, która musi zostać dodana, jest obliczenie:

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

Za każdym razem, gdy zostanie wybrana rotacja, metody manipulowania jednym palcem i dwoma palcami najpierw obsługują rotację. Jeśli zostanie wykryta jakakolwiek rotacja, składnik rotacji zostanie skutecznie usunięty. To, co pozostaje, jest interpretowane jako przesuwanie i skalowanie.

Oto OneFingerManipulate metoda. Jeśli obrót jednym palcem nie został włączony, logika jest prosta — po prostu używa poprzedniego punktu i nowego punktu do konstruowania wektora o nazwie delta odpowiadającej dokładnie translacji. Po włączeniu obrotu jednym palcem metoda używa kątów z punktu przestawnego (środka mapy bitowej) do poprzedniego punktu i nowego punktu do utworzenia macierzy obrotu:

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

W metodzie TwoFingerManipulate punkt przestawny jest położeniem palca, który nie porusza się w tym konkretnym zdarzeniu dotykowym. Obrót jest bardzo podobny do obrotu jednym palcem, a następnie wektor o nazwie oldVector (na podstawie poprzedniego punktu) jest dostosowywany do obrotu. Pozostały ruch jest interpretowany jako skalowanie:

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

Zauważysz, że w tej metodzie nie ma jawnego tłumaczenia. Jednak zarówno MakeRotation metody , jak i MakeScale są oparte na punkcie przestawnym, i obejmuje niejawne tłumaczenie. Jeśli używasz dwóch palców na mapie bitowej i przeciągając je w tym samym kierunku, TouchManipulation uzyskasz serię zdarzeń dotykowych naprzemiennych między dwoma palcami. Gdy każdy palec porusza się względem drugiego, skalowanie lub obracanie wyników, ale jest ono negowane przez ruch drugiego palca, a wynik jest tłumaczeniem.

Jedyną pozostałą częścią strony Manipulowanie dotykiem jest PaintSurface procedura obsługi w TouchManipulationPage pliku za pomocą kodu. Wywołuje to metodę Paint obiektu TouchManipulationBitmap, która stosuje macierz reprezentującą skumulowane działanie dotyku:

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

Procedura PaintSurface obsługi kończy się, wyświetlając MatrixDisplay obiekt pokazujący skumulowaną macierz dotykową:

Potrójny zrzut ekranu przedstawiający stronę Manipulowanie dotykiem

Manipulowanie wieloma mapami bitowymi

Jedną z zalet izolowania kodu przetwarzania dotykowego w klasach, takich jak TouchManipulationBitmap i TouchManipulationManager jest możliwość ponownego użycia tych klas w programie, który umożliwia użytkownikowi manipulowanie wieloma mapami bitowymi.

Na stronie Widok punktowy mapy bitowej pokazano, jak to zrobić. Zamiast definiować pole typu TouchManipulationBitmap, BitmapScatterPage klasa definiuje List obiekty mapy bitowej:

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

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

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

Konstruktor ładuje wszystkie mapy bitowe dostępne jako zasoby osadzone i dodaje je do elementu bitmapCollection. Zwróć uwagę, że Matrix właściwość jest inicjowana na każdym TouchManipulationBitmap obiekcie, więc lewe górne rogi każdej mapy bitowej są przesunięte o 100 pikseli.

Strona BitmapScatterView musi również obsługiwać zdarzenia dotykowe dla wielu map bitowych. Zamiast definiować List identyfikatory dotykowych aktualnie manipulowanych TouchManipulationBitmap obiektów, ten program wymaga słownika:

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

Zwróć uwagę, jak logika Pressed przechodzi przez odwrotnie bitmapCollection . Mapy bitowe często nakładają się na siebie. Mapy bitowe w dalszej części kolekcji wizualnie leżą na mapach bitowych wcześniej w kolekcji. Jeśli istnieje wiele map bitowych pod palcem, które naciskają na ekranie, najbardziej górny musi być ten, który jest manipulowany przez ten palec.

Zwróć również uwagę, że logika Pressed przenosi mapę bitową na koniec kolekcji, aby wizualnie przenosiła się do góry stosu innych map bitowych.

W zdarzeniach Moved i TouchAction Released program obsługi wywołuje metodę ProcessingTouchEvent w TouchManipulationBitmap sposób podobny do wcześniejszego programu.

PaintSurface Na koniec program obsługi wywołuje metodę Paint każdego TouchManipulationBitmap obiektu:

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

Kod przechodzi przez kolekcję i wyświetla stos map bitowych od początku kolekcji do końca:

Potrójny zrzut ekranu przedstawiający stronę Widok punktowy mapy bitowej

Skalowanie pojedynczego palca

Operacja skalowania zwykle wymaga gestu szczypania przy użyciu dwóch palców. Można jednak zaimplementować skalowanie za pomocą pojedynczego palca, przesuwając rogi mapy bitowej palcem.

Jest to pokazane na stronie Skalowanie pojedynczego palca narożnego. Ponieważ w tym przykładzie jest używany nieco inny typ skalowania niż zaimplementowany w TouchManipulationManager klasie, nie używa tej klasy ani TouchManipulationBitmap klasy. Zamiast tego cała logika dotykowa znajduje się w pliku za pomocą kodu. Jest to nieco prostsza logika niż zwykle, ponieważ śledzi tylko jeden palec naraz, i po prostu ignoruje wszelkie pomocnicze palce, które mogą dotykać ekranu.

Strona SingleFingerCornerScale.xaml tworzy wystąpienie SKCanvasView klasy i tworzy TouchEffect obiekt do śledzenia zdarzeń dotykowych:

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

Plik SingleFingerCornerScalePage.xaml.cs ładuje zasób mapy bitowej z katalogu Media i wyświetla go przy użyciu obiektu zdefiniowanego SKMatrix jako pole:

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

    public SingleFingerCornerScalePage()
    {
        InitializeComponent();

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

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

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

        canvas.Clear();

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

Ten SKMatrix obiekt jest modyfikowany przez logikę dotykową pokazaną poniżej.

Pozostała część pliku kodu jest procedurą TouchEffect obsługi zdarzeń. Zaczyna się od przekonwertowania bieżącej lokalizacji palca na SKPoint wartość. Pressed W przypadku typu akcji program obsługi sprawdza, czy żaden inny palec nie dotyka ekranu i że palec znajduje się w granicach mapy bitowej.

Kluczową częścią kodu jest instrukcja obejmująca if dwa wywołania Math.Pow metody . Ta matematyka sprawdza, czy lokalizacja palca znajduje się poza wielokropkiem, który wypełnia mapę bitową. Jeśli tak, jest to operacja skalowania. Palec znajduje się w pobliżu jednego z narożników mapy bitowej, a punkt obrotu określa się, że jest to przeciwny róg. Jeśli palec znajduje się w tym wielokropce, jest to regularna operacja przesuwania:

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

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

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

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

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

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

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

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

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

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

                SKMatrix matrix = SKMatrix.MakeIdentity();

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

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

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

Moved Typ akcji oblicza macierz odpowiadającą aktywności dotykowej od czasu naciśnięcia palca ekranu do tej pory. Łączy macierz z macierzą w efekcie w momencie, gdy palec po raz pierwszy nacisnął mapę bitową. Operacja skalowania jest zawsze względna względem rogu przeciwnego do tego, którego dotknął palec.

W przypadku małych lub podłużnych map bitowych wewnętrzny wielokropek może zajmować większość mapy bitowej i pozostawić małe obszary na rogach, aby skalować mapę bitową. Możesz wolisz nieco inne podejście, w takim przypadku możesz zastąpić cały if blok ustawiany isScaling na true następujący kod:

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

Ten kod skutecznie dzieli obszar mapy bitowej na wewnętrzny kształt rombu i cztery trójkąty w rogach. Pozwala to na chwytanie i skalowanie mapy bitowej znacznie większych obszarów w rogach.