Udostępnij za pośrednictwem


Przycinanie map bitowych SkiaSharp

W artykule Creating and Drawing SkiaSharp Bitmaps (Tworzenie i rysowanie map bitowych SkiaSharp) opisano sposób SKBitmap przekazywania obiektu do konstruktora SKCanvas . Każda metoda rysunku wywoływana na tej kanwie powoduje renderowanie grafiki na mapie bitowej. Metody rysowania obejmują DrawBitmapmetodę , co oznacza, że ta technika umożliwia przesyłanie części lub wszystkich jednej mapy bitowej do innej mapy bitowej, być może z zastosowanymi transformacjami.

Możesz użyć tej techniki do przycinania mapy bitowej, wywołując metodę DrawBitmap z prostokątami źródłowymi i docelowymi:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Jednak aplikacje implementujące przycinanie często udostępniają użytkownikowi interfejs do interaktywnego wybierania prostokąta przycinania:

Próbka przycinania

Ten artykuł koncentruje się na tym interfejsie.

Hermetyzowanie prostokąta przycinania

Warto odizolować część logiki przycinania w klasie o nazwie CroppingRectangle. Parametry konstruktora obejmują maksymalny prostokąt, który jest zazwyczaj rozmiarem przycinanej mapy bitowej i opcjonalnym współczynnikiem proporcji. Konstruktor najpierw definiuje początkowy prostokąt przycinania, który upublicznia we Rect właściwości typu SKRect. Ten początkowy prostokąt przycinania wynosi 80% szerokości i wysokości prostokąta mapy bitowej, ale jest dostosowywany, jeśli określono współczynnik proporcji:

class CroppingRectangle
{
    ···
    SKRect maxRect;             // generally the size of the bitmap
    float? aspectRatio;

    public CroppingRectangle(SKRect maxRect, float? aspectRatio = null)
    {
        this.maxRect = maxRect;
        this.aspectRatio = aspectRatio;

        // Set initial cropping rectangle
        Rect = new SKRect(0.9f * maxRect.Left + 0.1f * maxRect.Right,
                          0.9f * maxRect.Top + 0.1f * maxRect.Bottom,
                          0.1f * maxRect.Left + 0.9f * maxRect.Right,
                          0.1f * maxRect.Top + 0.9f * maxRect.Bottom);

        // Adjust for aspect ratio
        if (aspectRatio.HasValue)
        {
            SKRect rect = Rect;
            float aspect = aspectRatio.Value;

            if (rect.Width > aspect * rect.Height)
            {
                float width = aspect * rect.Height;
                rect.Left = (maxRect.Width - width) / 2;
                rect.Right = rect.Left + width;
            }
            else
            {
                float height = rect.Width / aspect;
                rect.Top = (maxRect.Height - height) / 2;
                rect.Bottom = rect.Top + height;
            }

            Rect = rect;
        }
    }

    public SKRect Rect { set; get; }
    ···
}

Jedną z przydatnych informacji, które CroppingRectangle również udostępniają, jest tablica SKPoint wartości odpowiadających czterem rogom prostokąta przycinania w kolejności w lewym górnym rogu, prawym górnym, prawym dolnym i lewym dolnym:

class CroppingRectangle
{
    ···
    public SKPoint[] Corners
    {
        get
        {
            return new SKPoint[]
            {
                new SKPoint(Rect.Left, Rect.Top),
                new SKPoint(Rect.Right, Rect.Top),
                new SKPoint(Rect.Right, Rect.Bottom),
                new SKPoint(Rect.Left, Rect.Bottom)
            };
        }
    }
    ···
}

Ta tablica jest używana w następującej metodzie, która jest nazywana HitTest. Parametr SKPoint jest punktem odpowiadającym dotykowi palca lub kliknięciu myszą. Metoda zwraca indeks (0, 1, 2 lub 3) odpowiadający narożnikowi, którego dotknął palec lub wskaźnik myszy w odległości podanej radius przez parametr :

class CroppingRectangle
{
    ···
    public int HitTest(SKPoint point, float radius)
    {
        SKPoint[] corners = Corners;

        for (int index = 0; index < corners.Length; index++)
        {
            SKPoint diff = point - corners[index];

            if ((float)Math.Sqrt(diff.X * diff.X + diff.Y * diff.Y) < radius)
            {
                return index;
            }
        }

        return -1;
    }
    ···
}

Jeśli dotyk lub wskaźnik myszy nie mieścił się w żadnym radius rogu, metoda zwraca wartość –1.

Ostateczna metoda w metodzie CroppingRectangle nosi nazwę MoveCorner, która jest wywoływana w odpowiedzi na ruch dotyku lub myszy. Dwa parametry wskazują indeks przenoszonego rogu i nową lokalizację tego rogu. Pierwsza połowa metody dostosowuje prostokąt przycinania na podstawie nowej lokalizacji rogu, ale zawsze w granicach maxRectelementu , który jest rozmiarem mapy bitowej. Ta logika uwzględnia również pole, MINIMUM aby uniknąć zwijania prostokąta przycinania w niczym:

class CroppingRectangle
{
    const float MINIMUM = 10;   // pixels width or height
    ···
    public void MoveCorner(int index, SKPoint point)
    {
        SKRect rect = Rect;

        switch (index)
        {
            case 0: // upper-left
                rect.Left = Math.Min(Math.Max(point.X, maxRect.Left), rect.Right - MINIMUM);
                rect.Top = Math.Min(Math.Max(point.Y, maxRect.Top), rect.Bottom - MINIMUM);
                break;

            case 1: // upper-right
                rect.Right = Math.Max(Math.Min(point.X, maxRect.Right), rect.Left + MINIMUM);
                rect.Top = Math.Min(Math.Max(point.Y, maxRect.Top), rect.Bottom - MINIMUM);
                break;

            case 2: // lower-right
                rect.Right = Math.Max(Math.Min(point.X, maxRect.Right), rect.Left + MINIMUM);
                rect.Bottom = Math.Max(Math.Min(point.Y, maxRect.Bottom), rect.Top + MINIMUM);
                break;

            case 3: // lower-left
                rect.Left = Math.Min(Math.Max(point.X, maxRect.Left), rect.Right - MINIMUM);
                rect.Bottom = Math.Max(Math.Min(point.Y, maxRect.Bottom), rect.Top + MINIMUM);
                break;
        }

        // Adjust for aspect ratio
        if (aspectRatio.HasValue)
        {
            float aspect = aspectRatio.Value;

            if (rect.Width > aspect * rect.Height)
            {
                float width = aspect * rect.Height;

                switch (index)
                {
                    case 0:
                    case 3: rect.Left = rect.Right - width; break;
                    case 1:
                    case 2: rect.Right = rect.Left + width; break;
                }
            }
            else
            {
                float height = rect.Width / aspect;

                switch (index)
                {
                    case 0:
                    case 1: rect.Top = rect.Bottom - height; break;
                    case 2:
                    case 3: rect.Bottom = rect.Top + height; break;
                }
            }
        }

        Rect = rect;
    }
}

Druga połowa metody dostosowuje się do opcjonalnego współczynnika proporcji.

Pamiętaj, że wszystko w tej klasie jest w jednostkach pikseli.

Widok kanwy tylko do przycinania

Właśnie CroppingRectangle widziana klasa jest używana przez klasę PhotoCropperCanvasView , która pochodzi z klasy SKCanvasView. Ta klasa jest odpowiedzialna za wyświetlanie mapy bitowej i prostokąta przycinania, a także obsługę zdarzeń dotykowych lub myszy w celu zmiany prostokąta przycinania.

Konstruktor PhotoCropperCanvasView wymaga mapy bitowej. Współczynnik proporcji jest opcjonalny. Konstruktor tworzy wystąpienie obiektu typu CroppingRectangle na podstawie tej mapy bitowej i współczynnika proporcji i zapisuje go jako pole:

class PhotoCropperCanvasView : SKCanvasView
{
    ···
    SKBitmap bitmap;
    CroppingRectangle croppingRect;
    ···
    public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
    {
        this.bitmap = bitmap;

        SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
        croppingRect = new CroppingRectangle(bitmapRect, aspectRatio);
        ···
    }
    ···
}

Ponieważ ta klasa pochodzi z SKCanvasViewklasy , nie musi instalować programu obsługi dla PaintSurface zdarzenia. Zamiast tego może zastąpić jego OnPaintSurface metodę. Metoda wyświetla mapę bitową i używa kilku obiektów zapisanych SKPaint jako pola, aby narysować bieżący prostokąt przycinania:

class PhotoCropperCanvasView : SKCanvasView
{
    const int CORNER = 50;      // pixel length of cropper corner
    ···
    SKBitmap bitmap;
    CroppingRectangle croppingRect;
    SKMatrix inverseBitmapMatrix;
    ···
    // Drawing objects
    SKPaint cornerStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.White,
        StrokeWidth = 10
    };

    SKPaint edgeStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.White,
        StrokeWidth = 2
    };
    ···
    protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
    {
        base.OnPaintSurface(args);

        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Gray);

        // Calculate rectangle for displaying bitmap
        float scale = Math.Min((float)info.Width / bitmap.Width, (float)info.Height / bitmap.Height);
        float x = (info.Width - scale * bitmap.Width) / 2;
        float y = (info.Height - scale * bitmap.Height) / 2;
        SKRect bitmapRect = new SKRect(x, y, x + scale * bitmap.Width, y + scale * bitmap.Height);
        canvas.DrawBitmap(bitmap, bitmapRect);

        // Calculate a matrix transform for displaying the cropping rectangle
        SKMatrix bitmapScaleMatrix = SKMatrix.MakeIdentity();
        bitmapScaleMatrix.SetScaleTranslate(scale, scale, x, y);

        // Display rectangle
        SKRect scaledCropRect = bitmapScaleMatrix.MapRect(croppingRect.Rect);
        canvas.DrawRect(scaledCropRect, edgeStroke);

        // Display heavier corners
        using (SKPath path = new SKPath())
        {
            path.MoveTo(scaledCropRect.Left, scaledCropRect.Top + CORNER);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Left + CORNER, scaledCropRect.Top);

            path.MoveTo(scaledCropRect.Right - CORNER, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Top + CORNER);

            path.MoveTo(scaledCropRect.Right, scaledCropRect.Bottom - CORNER);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Right - CORNER, scaledCropRect.Bottom);

            path.MoveTo(scaledCropRect.Left + CORNER, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom - CORNER);

            canvas.DrawPath(path, cornerStroke);
        }

        // Invert the transform for touch tracking
        bitmapScaleMatrix.TryInvert(out inverseBitmapMatrix);
    }
    ···
}

Kod w CroppingRectangle klasie opiera prostokąt przycinania na rozmiarze pikseli mapy bitowej. Jednak wyświetlanie mapy bitowej według PhotoCropperCanvasView klasy jest skalowane na podstawie rozmiaru obszaru wyświetlania. Obliczona bitmapScaleMatrix w OnPaintSurface przesłonięć mapowanie z pikseli mapy bitowej na rozmiar i położenie mapy bitowej w miarę wyświetlania. Następnie ta macierz służy do przekształcania prostokąta przycinania, aby można było go wyświetlić względem mapy bitowej.

Ostatni wiersz OnPaintSurface przesłonięcia przyjmuje odwrotność bitmapScaleMatrix obiektu i zapisuje go jako inverseBitmapMatrix pole. Jest to używane do przetwarzania dotykowego.

TouchEffect Obiekt jest tworzone jako pole, a konstruktor dołącza program obsługi do TouchAction zdarzenia, ale TouchEffect należy go dodać do Effects kolekcji elementu nadrzędnego pochodnegoSKCanvasView, aby wykonać OnParentSet przesłonięcia:

class PhotoCropperCanvasView : SKCanvasView
{
    ···
    const int RADIUS = 100;     // pixel radius of touch hit-test
    ···
    CroppingRectangle croppingRect;
    SKMatrix inverseBitmapMatrix;

    // Touch tracking
    TouchEffect touchEffect = new TouchEffect();
    struct TouchPoint
    {
        public int CornerIndex { set; get; }
        public SKPoint Offset { set; get; }
    }

    Dictionary<long, TouchPoint> touchPoints = new Dictionary<long, TouchPoint>();
    ···
    public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
    {
        ···
        touchEffect.TouchAction += OnTouchEffectTouchAction;
    }
    ···
    protected override void OnParentSet()
    {
        base.OnParentSet();

        // Attach TouchEffect to parent view
        Parent.Effects.Add(touchEffect);
    }
    ···
    void OnTouchEffectTouchAction(object sender, TouchActionEventArgs args)
    {
        SKPoint pixelLocation = ConvertToPixel(args.Location);
        SKPoint bitmapLocation = inverseBitmapMatrix.MapPoint(pixelLocation);

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Convert radius to bitmap/cropping scale
                float radius = inverseBitmapMatrix.ScaleX * RADIUS;

                // Find corner that the finger is touching
                int cornerIndex = croppingRect.HitTest(bitmapLocation, radius);

                if (cornerIndex != -1 && !touchPoints.ContainsKey(args.Id))
                {
                    TouchPoint touchPoint = new TouchPoint
                    {
                        CornerIndex = cornerIndex,
                        Offset = bitmapLocation - croppingRect.Corners[cornerIndex]
                    };

                    touchPoints.Add(args.Id, touchPoint);
                }
                break;

            case TouchActionType.Moved:
                if (touchPoints.ContainsKey(args.Id))
                {
                    TouchPoint touchPoint = touchPoints[args.Id];
                    croppingRect.MoveCorner(touchPoint.CornerIndex,
                                            bitmapLocation - touchPoint.Offset);
                    InvalidateSurface();
                }
                break;

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

    SKPoint ConvertToPixel(Xamarin.Forms.Point pt)
    {
        return new SKPoint((float)(CanvasSize.Width * pt.X / Width),
                           (float)(CanvasSize.Height * pt.Y / Height));
    }
}

Zdarzenia dotykowe przetwarzane przez TouchAction program obsługi znajdują się w jednostkach niezależnych od urządzenia. Najpierw należy je przekonwertować na piksele przy użyciu ConvertToPixel metody w dolnej części klasy, a następnie przekonwertować na CroppingRectangle jednostki przy użyciu metody inverseBitmapMatrix.

W przypadku Pressed zdarzeń TouchAction program obsługi wywołuje metodę HitTest CroppingRectangle. Jeśli zwraca to indeks inny niż –1, jednym z narożników prostokąta przycinania jest manipulowanie. Ten indeks i przesunięcie rzeczywistego punktu dotykowego z rogu jest przechowywane w TouchPoint obiekcie i dodawane do słownika touchPoints .

Moved W przypadku zdarzenia MoveCorner metoda CroppingRectangle metody jest wywoływana w celu przeniesienia rogu z możliwymi korektami współczynnika proporcji.

W dowolnym momencie program używający PhotoCropperCanvasView programu może uzyskać dostęp do CroppedBitmap właściwości. Ta właściwość używa Rect właściwości , CroppingRectangle aby utworzyć nową mapę bitową przyciętego rozmiaru. Następnie wersja z DrawBitmap prostokątami docelowymi i źródłowymi wyodrębnia podzbiór oryginalnej mapy bitowej:

class PhotoCropperCanvasView : SKCanvasView
{
    ···
    SKBitmap bitmap;
    CroppingRectangle croppingRect;
    ···
    public SKBitmap CroppedBitmap
    {
        get
        {
            SKRect cropRect = croppingRect.Rect;
            SKBitmap croppedBitmap = new SKBitmap((int)cropRect.Width,
                                                  (int)cropRect.Height);
            SKRect dest = new SKRect(0, 0, cropRect.Width, cropRect.Height);
            SKRect source = new SKRect(cropRect.Left, cropRect.Top,
                                       cropRect.Right, cropRect.Bottom);

            using (SKCanvas canvas = new SKCanvas(croppedBitmap))
            {
                canvas.DrawBitmap(bitmap, source, dest);
            }

            return croppedBitmap;
        }
    }
    ···
}

Hostowanie widoku kanwy przycinania zdjęć

W przypadku tych dwóch klas obsługujących logikę przycinania strona Przycinanie zdjęć w przykładowej aplikacji ma bardzo mało pracy. Plik XAML tworzy wystąpienie elementu w Grid celu hostowania PhotoCropperCanvasView przycisku i Gotowe :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SkiaSharpFormsDemos.Bitmaps.PhotoCroppingPage"
             Title="Photo Cropping">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid x:Name="canvasViewHost"
              Grid.Row="0"
              BackgroundColor="Gray"
              Padding="5" />

        <Button Text="Done"
                Grid.Row="1"
                HorizontalOptions="Center"
                Margin="5"
                Clicked="OnDoneButtonClicked" />
    </Grid>
</ContentPage>

Nie PhotoCropperCanvasView można utworzyć wystąpienia obiektu w pliku XAML, ponieważ wymaga parametru typu SKBitmap.

Zamiast tego element PhotoCropperCanvasView jest tworzone w konstruktorze pliku kodu za pomocą jednej z map bitowych zasobów:

public partial class PhotoCroppingPage : ContentPage
{
    PhotoCropperCanvasView photoCropper;
    SKBitmap croppedBitmap;

    public PhotoCroppingPage ()
    {
        InitializeComponent ();

        SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(GetType(),
            "SkiaSharpFormsDemos.Media.MountainClimbers.jpg");

        photoCropper = new PhotoCropperCanvasView(bitmap);
        canvasViewHost.Children.Add(photoCropper);
    }

    void OnDoneButtonClicked(object sender, EventArgs args)
    {
        croppedBitmap = photoCropper.CroppedBitmap;

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

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

        canvas.Clear();
        canvas.DrawBitmap(croppedBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

Użytkownik może następnie manipulować prostokątem przycinania:

Zdjęcie Przycinanie 1

Po zdefiniowaniu dobrego przycinania prostokąta kliknij przycisk Gotowe . Procedura Clicked obsługi uzyskuje przyciętą mapę bitową z CroppedBitmap właściwości PhotoCropperCanvasViewi zastępuje całą zawartość strony nowym SKCanvasView obiektem, który wyświetla tę przyciętą mapę bitową:

Zdjęcie Przycinanie 2

Spróbuj ustawić drugi argument PhotoCropperCanvasView 1.78f (na przykład):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Zobaczysz prostokąt przycinania ograniczony do 16-do-9 współczynnik proporcji telewizora o wysokiej rozdzielczości.

Dzielenie mapy bitowej na kafelki

Wersja Xamarin.Forms słynnej zagadki 14-15 pojawiła się w rozdziale 22 książki Creating Mobile Apps with Xamarin.Formsi można ją pobrać jako XamagonXuzzle. Jednak zagadka staje się bardziej zabawna (i często trudniejsza), gdy jest oparta na obrazie z własnej biblioteki zdjęć.

Ta wersja układanki 14-15 jest częścią przykładowej aplikacji i składa się z serii stron zatytułowanych Photo Puzzle.

Plik PhotoPuzzlePage1.xaml składa się z elementu Button:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SkiaSharpFormsDemos.Bitmaps.PhotoPuzzlePage1"
             Title="Photo Puzzle">

    <Button Text="Pick a photo from your library"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand"
            Clicked="OnPickButtonClicked"/>

</ContentPage>

Plik związany z kodem implementuje Clicked program obsługi, który używa IPhotoLibrary usługi zależności, aby umożliwić użytkownikowi wybranie zdjęcia z biblioteki zdjęć:

public partial class PhotoPuzzlePage1 : ContentPage
{
    public PhotoPuzzlePage1 ()
    {
        InitializeComponent ();
    }

    async void OnPickButtonClicked(object sender, EventArgs args)
    {
        IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();
        using (Stream stream = await photoLibrary.PickPhotoAsync())
        {
            if (stream != null)
            {
                SKBitmap bitmap = SKBitmap.Decode(stream);

                await Navigation.PushAsync(new PhotoPuzzlePage2(bitmap));
            }
        }
    }
}

Następnie metoda przechodzi do PhotoPuzzlePage2metody , przechodząc do constuctor wybranej mapy bitowej.

Możliwe, że zdjęcie wybrane z biblioteki nie jest zorientowane, jak pokazano w bibliotece zdjęć, ale jest obracane lub do góry nogami. (Jest to szczególnie problem z urządzeniami z systemem iOS). Z tego powodu PhotoPuzzlePage2 można obrócić obraz do żądanej orientacji. Plik XAML zawiera trzy przyciski oznaczone etykietą 90° w prawo (czyli zgodnie z ruchem wskazówek zegara), 90° w lewo (odwrotnie) i Gotowe.

Plik za kodem implementuje logikę obrotu mapy bitowej pokazaną w artykule Tworzenie i rysowanie na mapach bitowych SkiaSharp. Użytkownik może obrócić obraz o 90 stopni zgodnie z ruchem wskazówek zegara lub wskazówek zegara dowolną liczbę razy:

public partial class PhotoPuzzlePage2 : ContentPage
{
    SKBitmap bitmap;

    public PhotoPuzzlePage2 (SKBitmap bitmap)
    {
        this.bitmap = bitmap;

        InitializeComponent ();
    }

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

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
    }

    void OnRotateRightButtonClicked(object sender, EventArgs args)
    {
        SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear();
            canvas.Translate(bitmap.Height, 0);
            canvas.RotateDegrees(90);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = rotatedBitmap;
        canvasView.InvalidateSurface();
    }

    void OnRotateLeftButtonClicked(object sender, EventArgs args)
    {
        SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear();
            canvas.Translate(0, bitmap.Width);
            canvas.RotateDegrees(-90);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = rotatedBitmap;
        canvasView.InvalidateSurface();
    }

    async void OnDoneButtonClicked(object sender, EventArgs args)
    {
        await Navigation.PushAsync(new PhotoPuzzlePage3(bitmap));
    }
}

Gdy użytkownik kliknie przycisk Gotowe , Clicked program obsługi przechodzi do PhotoPuzzlePage3elementu , przekazując ostatnią obróconą mapę bitową w konstruktorze strony.

PhotoPuzzlePage3 umożliwia przycięcie zdjęcia. Program wymaga kwadratowej mapy bitowej, aby podzielić się na siatkę 4-by-4 kafelków.

Plik PhotoPuzzlePage3.xaml zawiera Labelelement , a Grid do hostowania PhotoCropperCanvasViewelementu i inny przycisk Gotowe :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SkiaSharpFormsDemos.Bitmaps.PhotoPuzzlePage3"
             Title="Photo Puzzle">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Label Text="Crop the photo to a square"
               Grid.Row="0"
               FontSize="Large"
               HorizontalTextAlignment="Center"
               Margin="5" />

        <Grid x:Name="canvasViewHost"
              Grid.Row="1"
              BackgroundColor="Gray"
              Padding="5" />

        <Button Text="Done"
                Grid.Row="2"
                HorizontalOptions="Center"
                Margin="5"
                Clicked="OnDoneButtonClicked" />
    </Grid>
</ContentPage>

Plik za kodem tworzy wystąpienie PhotoCropperCanvasView elementu z mapą bitową przekazaną do konstruktora. Zwróć uwagę, że wartość 1 jest przekazywana jako drugi argument do PhotoCropperCanvasView. Ten współczynnik proporcji 1 wymusza przycinanie prostokąta na kwadrat:

public partial class PhotoPuzzlePage3 : ContentPage
{
    PhotoCropperCanvasView photoCropper;

    public PhotoPuzzlePage3(SKBitmap bitmap)
    {
        InitializeComponent ();

        photoCropper = new PhotoCropperCanvasView(bitmap, 1f);
        canvasViewHost.Children.Add(photoCropper);
    }

    async void OnDoneButtonClicked(object sender, EventArgs args)
    {
        SKBitmap croppedBitmap = photoCropper.CroppedBitmap;
        int width = croppedBitmap.Width / 4;
        int height = croppedBitmap.Height / 4;

        ImageSource[] imgSources = new ImageSource[15];

        for (int row = 0; row < 4; row++)
        {
            for (int col = 0; col < 4; col++)
            {
                // Skip the last one!
                if (row == 3 && col == 3)
                    break;

                // Create a bitmap 1/4 the width and height of the original
                SKBitmap bitmap = new SKBitmap(width, height);
                SKRect dest = new SKRect(0, 0, width, height);
                SKRect source = new SKRect(col * width, row * height, (col + 1) * width, (row + 1) * height);

                // Copy 1/16 of the original into that bitmap
                using (SKCanvas canvas = new SKCanvas(bitmap))
                {
                    canvas.DrawBitmap(croppedBitmap, source, dest);
                }

                imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
            }
        }

        await Navigation.PushAsync(new PhotoPuzzlePage4(imgSources));
    }
}

Procedura obsługi przycisku Gotowe uzyskuje szerokość i wysokość przyciętej mapy bitowej (te dwie wartości powinny być takie same), a następnie dzieli ją na 15 oddzielnych map bitowych, z których każda ma 1/4 szerokość i wysokość oryginału. (Nie utworzono ostatniej z możliwych 16 map bitowych). Metoda DrawBitmap z prostokątem źródłowym i docelowym umożliwia utworzenie mapy bitowej na podstawie podzbioru większej mapy bitowej.

Konwertowanie na Xamarin.Forms mapy bitowe

W metodzie OnDoneButtonClicked tablica utworzona dla 15 map bitowych ma typ ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource jest typem Xamarin.Forms podstawowym, który hermetyzuje mapę bitową. Na szczęście SkiaSharp umożliwia konwertowanie map bitowych SkiaSharp na Xamarin.Forms mapy bitowe. Zestaw SkiaSharp.Views.Forms definiuje klasę pochodzącą SKBitmapImageSource z ImageSource klasy , ale można ją utworzyć na podstawie obiektu SkiaSharp SKBitmap . SKBitmapImageSource Definiuje nawet konwersje między SKBitmapImageSource i SKBitmap, a w ten sposób SKBitmap obiekty są przechowywane w tablicy jako Xamarin.Forms mapy bitowe:

imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;

Ta tablica map bitowych jest przekazywana jako konstruktor do PhotoPuzzlePage4. Ta strona jest całkowicie Xamarin.Forms i nie używa żadnych SkiaSharp. Jest on bardzo podobny do XamagonXuzzle, więc nie zostanie opisany tutaj, ale wyświetla wybrane zdjęcie podzielone na 15 kafelków kwadratowych:

Zdjęcie Puzzle 1

Naciśnięcie przycisku Randomize powoduje wymieszanie wszystkich kafelków:

Zdjęcie Puzzle 2

Teraz możesz umieścić je z powrotem w odpowiedniej kolejności. Wszystkie kafelki w tym samym wierszu lub kolumnie co pusty kwadrat można wykorzystać, aby przenieść je do pustego kwadratu.