Ritaglio di bitmap skiaSharp

L'articolo Creazione e disegno di bitmap SkiaSharp descrive come passare un SKBitmap oggetto a un SKCanvas costruttore. Qualsiasi metodo di disegno chiamato in tale area di disegno fa sì che venga eseguito il rendering della grafica nella bitmap. Questi metodi di disegno includono DrawBitmap, il che significa che questa tecnica consente il trasferimento di parte o di una bitmap a un'altra bitmap, ad esempio con trasformazioni applicate.

È possibile usare questa tecnica per ritagliare una bitmap chiamando il DrawBitmap metodo con rettangoli di origine e di destinazione:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Tuttavia, le applicazioni che implementano il ritaglio spesso forniscono un'interfaccia per consentire all'utente di selezionare in modo interattivo il rettangolo di ritaglio:

Esempio di ritaglio

Questo articolo è incentrato su tale interfaccia.

Incapsulamento del rettangolo di ritaglio

È utile isolare alcune delle logiche di ritaglio in una classe denominata CroppingRectangle. I parametri del costruttore includono un rettangolo massimo, che è in genere la dimensione della bitmap ritagliata e le proporzioni facoltative. Il costruttore definisce innanzitutto un rettangolo di ritaglio iniziale, che rende pubblico nella Rect proprietà di tipo SKRect. Questo rettangolo di ritaglio iniziale è l'80% della larghezza e dell'altezza del rettangolo bitmap, ma viene quindi regolato se viene specificato un rapporto di aspetto:

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

Una parte utile di informazioni che CroppingRectangle rende disponibile è una matrice di SKPoint valori corrispondente ai quattro angoli del rettangolo di ritaglio nell'ordine superiore sinistro, superiore destro, inferiore destro e inferiore sinistro:

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

Questa matrice viene usata nel metodo seguente, denominato HitTest. Il SKPoint parametro è un punto corrispondente a un tocco del dito o a un clic del mouse. Il metodo restituisce un indice (0, 1, 2 o 3) corrispondente all'angolo toccato dal dito o dal puntatore del mouse, entro una distanza specificata dal radius parametro :

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

Se il tocco o il punto del mouse non si trova in radius unità di qualsiasi angolo, il metodo restituisce –1.

Il metodo finale in CroppingRectangle viene chiamato , che viene chiamato MoveCornerin risposta allo spostamento del tocco o del mouse. I due parametri indicano l'indice dell'angolo da spostare e la nuova posizione dell'angolo. La prima metà del metodo regola il rettangolo di ritaglio in base alla nuova posizione dell'angolo, ma sempre all'interno dei limiti di maxRect, ovvero le dimensioni della bitmap. Questa logica tiene conto anche del MINIMUM campo per evitare la compressione del rettangolo di ritaglio in nulla:

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

La seconda metà del metodo viene modificata per le proporzioni facoltative.

Tenere presente che tutto ciò che si trova in questa classe è espresso in unità di pixel.

Una visualizzazione canvas solo per il ritaglio

La CroppingRectangle classe appena visualizzata viene usata dalla PhotoCropperCanvasView classe , che deriva da SKCanvasView. Questa classe è responsabile della visualizzazione della bitmap e del rettangolo di ritaglio, nonché della gestione degli eventi tocco o mouse per la modifica del rettangolo di ritaglio.

Il PhotoCropperCanvasView costruttore richiede una bitmap. Le proporzioni sono facoltative. Il costruttore crea un'istanza di un oggetto di tipo CroppingRectangle in base a questa bitmap e alle proporzioni e lo salva come campo:

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

Poiché questa classe deriva da SKCanvasView, non è necessario installare un gestore per l'evento PaintSurface . Può invece eseguire l'override del relativo OnPaintSurface metodo. Il metodo visualizza la bitmap e usa un paio di SKPaint oggetti salvati come campi per disegnare il rettangolo di ritaglio corrente:

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

Il codice nella CroppingRectangle classe basa il rettangolo di ritaglio sulla dimensione in pixel della bitmap. Tuttavia, la visualizzazione della bitmap in base alla PhotoCropperCanvasView classe viene ridimensionata in base alle dimensioni dell'area di visualizzazione. L'oggetto bitmapScaleMatrix calcolato nell'override esegue il OnPaintSurface mapping dai pixel bitmap alle dimensioni e alla posizione della bitmap così come viene visualizzata. Questa matrice viene quindi usata per trasformare il rettangolo di ritaglio in modo che possa essere visualizzato rispetto alla bitmap.

L'ultima riga dell'override OnPaintSurface accetta l'inverso di bitmapScaleMatrix e la salva come inverseBitmapMatrix campo. Viene usato per l'elaborazione del tocco.

Viene TouchEffect creata un'istanza di un oggetto come campo e il costruttore associa un gestore all'evento TouchAction , ma deve TouchEffect essere aggiunto alla Effects raccolta dell'elemento padre del SKCanvasView derivato, in modo che venga eseguito nell'override OnParentSet :

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

Gli eventi di tocco elaborati dal gestore sono in unità indipendenti dal TouchAction dispositivo. Questi devono prima essere convertiti in pixel usando il ConvertToPixel metodo nella parte inferiore della classe e quindi convertiti in CroppingRectangle unità usando inverseBitmapMatrix.

Per Pressed gli eventi, il TouchAction gestore chiama il HitTest metodo di CroppingRectangle. Se restituisce un indice diverso da –1, viene modificato uno degli angoli del rettangolo di ritaglio. Tale indice e un offset del punto di tocco effettivo dall'angolo vengono archiviati in un TouchPoint oggetto e aggiunti al touchPoints dizionario.

Per l'evento Moved , viene chiamato il MoveCorner metodo di CroppingRectangle per spostare l'angolo, con possibili regolazioni per le proporzioni.

In qualsiasi momento, un programma che usa PhotoCropperCanvasView può accedere alla CroppedBitmap proprietà . Questa proprietà usa la Rect proprietà di CroppingRectangle per creare una nuova bitmap delle dimensioni ritagliate. La versione di con rettangoli di DrawBitmap destinazione e di origine estrae quindi un subset della bitmap originale:

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

Hosting della visualizzazione canvas di ritaglio foto

Con queste due classi che gestiscono la logica di ritaglio, la pagina Ritaglio foto nell'applicazione di esempio ha molto poco lavoro da eseguire. Il file XAML crea un'istanza Grid di per ospitare e PhotoCropperCanvasView un pulsante Fine :

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

Non PhotoCropperCanvasView è possibile creare un'istanza nel file XAML perché richiede un parametro di tipo SKBitmap.

Viene invece creata un'istanza PhotoCropperCanvasView di nel costruttore del file code-behind usando una delle bitmap di risorsa:

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

L'utente può quindi modificare il rettangolo di ritaglio:

Ritaglio foto 1

Quando è stato definito un rettangolo di ritaglio corretto, fare clic sul pulsante Fine . Il Clicked gestore ottiene la bitmap ritagliata dalla CroppedBitmap proprietà di PhotoCropperCanvasViewe sostituisce tutto il contenuto della pagina con un nuovo SKCanvasView oggetto che visualizza questa bitmap ritagliata:

Foto Ritaglio 2

Provare a impostare il secondo argomento di PhotoCropperCanvasView su 1,78f (ad esempio):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Vedrai il rettangolo di ritaglio limitato a una caratteristica delle proporzioni da 16 a 9 della televisione ad alta definizione.

Divisione di una bitmap in riquadri

Una Xamarin.Forms versione del famoso puzzle 14-15 è apparso nel capitolo 22 del libro Creating Mobile Apps withXamarin.Formse può essere scaricato come XamagonXuzzle. Tuttavia, il puzzle diventa più divertente (e spesso più impegnativo) quando si basa su un'immagine dalla tua libreria di foto.

Questa versione del puzzle 14-15 fa parte dell'applicazione di esempio ed è costituita da una serie di pagine intitolate Photo Puzzle.

Il file PhotoPuzzlePage1.xaml è costituito da :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>

Il file code-behind implementa un Clicked gestore che usa il IPhotoLibrary servizio di dipendenza per consentire all'utente di selezionare una foto dalla raccolta foto:

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

Il metodo passa quindi a PhotoPuzzlePage2, passando al constuctor la bitmap selezionata.

È possibile che la foto selezionata dalla biblioteca non sia orientata come appare nella raccolta foto, ma è ruotata o rovesciata. Si tratta in particolare di un problema con i dispositivi iOS. Per questo motivo, PhotoPuzzlePage2 consente di ruotare l'immagine su un orientamento desiderato. Il file XAML contiene tre pulsanti etichettati a 90° a destra (ovvero in senso orario), a 90° a sinistra (antiorario) e a fine.

Il file code-behind implementa la logica di rotazione bitmap illustrata nell'articolo Creazione e disegno su bitmap SkiaSharp. L'utente può ruotare l'immagine di 90 gradi in senso orario o in senso antiorario qualsiasi numero di volte:

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

Quando l'utente fa clic sul pulsante Fine , il Clicked gestore passa a PhotoPuzzlePage3, passando la bitmap ruotata finale nel costruttore della pagina.

PhotoPuzzlePage3 consente di ritagliare la foto. Il programma richiede una bitmap quadrata per dividere in una griglia di riquadri da 4 a 4.

Il file PhotoPuzzlePage3.xaml contiene un oggetto Label, un Grid oggetto per ospitare e PhotoCropperCanvasViewun altro pulsante Fine :

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

Il file code-behind crea un'istanza di PhotoCropperCanvasView con la bitmap passata al relativo costruttore. Si noti che un valore 1 viene passato come secondo argomento a PhotoCropperCanvasView. Questa proporzioni di 1 forza il rettangolo di ritaglio come quadrato:

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

Il gestore pulsante Fine ottiene la larghezza e l'altezza della bitmap ritagliata (questi due valori devono essere uguali) e quindi lo divide in 15 bitmap separate, ognuna delle quali è 1/4 la larghezza e l'altezza dell'originale. L'ultima delle possibili 16 bitmap non viene creata. Il DrawBitmap metodo con rettangolo di origine e di destinazione consente di creare una bitmap in base al subset di una bitmap più grande.

Conversione in Xamarin.Forms bitmap

OnDoneButtonClicked Nel metodo la matrice creata per le 15 bitmap è di tipo ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource è il Xamarin.Forms tipo di base che incapsula una bitmap. Fortunatamente, SkiaSharp consente la conversione da bitmap SkiaSharp a Xamarin.Forms bitmap. L'assembly SkiaSharp.Views.Forms definisce una SKBitmapImageSource classe che deriva da ImageSource ma che può essere creata in base a un oggetto SkiaSharpSKBitmap. SKBitmapImageSource definisce anche le conversioni tra SKBitmapImageSource e SKBitmaped è così che SKBitmap gli oggetti vengono archiviati in una matrice come Xamarin.Forms bitmap:

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

Questa matrice di bitmap viene passata come costruttore a PhotoPuzzlePage4. Questa pagina è interamente Xamarin.Forms e non usa skiaSharp. È molto simile a XamagonXuzzle, quindi non verrà descritto qui, ma visualizza la foto selezionata divisa in 15 riquadri quadrati:

Foto Puzzle 1

Premendo il pulsante Casuali si combinano tutti i riquadri:

Foto Puzzle 2

Ora è possibile rimetterli nell'ordine corretto. Tutti i riquadri nella stessa riga o colonna del quadrato vuoto possono essere toccati per spostarli nel quadrato vuoto.