Partilhar via


Recortando bitmaps SkiaSharp

O artigo Criando e desenhando bitmaps do SkiaSharp descreveu como um SKBitmap objeto pode ser passado para um SKCanvas construtor. Qualquer método de desenho chamado nessa tela faz com que os gráficos sejam renderizados no bitmap. Esses métodos de desenho incluem DrawBitmap, o que significa que essa técnica permite transferir parte ou todo um bitmap para outro bitmap, talvez com transformações aplicadas.

Você pode usar essa técnica para cortar um bitmap chamando o DrawBitmap método com retângulos de origem e destino:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

No entanto, os aplicativos que implementam o corte geralmente fornecem uma interface para o usuário selecionar interativamente o retângulo de corte:

Amostra de corte

Este artigo se concentra nessa interface.

Encapsulando o retângulo de corte

É útil isolar parte da lógica de corte em uma classe chamada CroppingRectangle. Os parâmetros do construtor incluem um retângulo máximo, que geralmente é o tamanho do bitmap que está sendo cortado, e uma taxa de proporção opcional. O construtor primeiro define um retângulo de corte inicial, que ele torna público na Rect propriedade do tipo SKRect. Esse retângulo de corte inicial tem 80% da largura e da altura do retângulo de bitmap, mas é ajustado se uma taxa de proporção for especificada:

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

Uma informação útil que CroppingRectangle também disponibiliza é uma matriz de SKPoint valores correspondentes aos quatro cantos do retângulo de corte na ordem superior esquerda, superior direita, inferior direita e inferior esquerda:

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

Essa matriz é usada no método a seguir, chamado HitTest. O SKPoint parâmetro é um ponto correspondente a um toque do dedo ou um clique do mouse. O método retorna um índice (0, 1, 2 ou 3) correspondente ao canto que o dedo ou o ponteiro do mouse tocou, dentro de radius uma distância dada pelo parâmetro:

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 o ponto de toque ou mouse não estiver dentro radius de unidades de nenhum canto, o método retornará –1.

O método final é CroppingRectangle chamado MoveCorner, que é chamado em resposta ao toque ou ao movimento do mouse. Os dois parâmetros indicam o índice do canto que está sendo movido e o novo local desse canto. A primeira metade do método ajusta o retângulo de corte com base no novo local do canto, mas sempre dentro dos limites de maxRect, que é o tamanho do bitmap. Essa lógica também leva em conta o MINIMUM campo para evitar o recolhimento do retângulo de corte em nada:

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

A segunda metade do método se ajusta à proporção opcional.

Lembre-se de que tudo nesta classe está em unidades de pixels.

Uma visualização de tela apenas para recortar

A CroppingRectangle classe que você acabou de ver é usada pela classe, que deriva PhotoCropperCanvasView de SKCanvasView. Essa classe é responsável por exibir o bitmap e o retângulo de corte, bem como manipular eventos de toque ou mouse para alterar o retângulo de corte.

O PhotoCropperCanvasView construtor requer um bitmap. Uma proporção é opcional. O construtor instancia um objeto do tipo CroppingRectangle com base nesse bitmap e proporção e o salva como um 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);
        ···
    }
    ···
}

Como essa classe deriva de SKCanvasView, ela não precisa instalar um manipulador para o PaintSurface evento. Em vez disso, ele pode substituir seu OnPaintSurface método. O método exibe o bitmap e usa alguns objetos salvos como campos para desenhar o retângulo de SKPaint corte atual:

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

O código na CroppingRectangle classe baseia o retângulo de corte no tamanho do pixel do bitmap. No entanto, a exibição do bitmap pela PhotoCropperCanvasView classe é dimensionada com base no tamanho da área de exibição. O bitmapScaleMatrix calculado na OnPaintSurface substituição é mapeado dos pixels de bitmap para o tamanho e a posição do bitmap conforme ele é exibido. Essa matriz é usada para transformar o retângulo de corte para que ele possa ser exibido em relação ao bitmap.

A última linha da substituição pega OnPaintSurface o inverso do bitmapScaleMatrix e o salva como o inverseBitmapMatrix campo. Isso é usado para processamento de toque.

Um TouchEffect objeto é instanciado como um campo e o construtor anexa um manipulador ao TouchAction evento, mas ele TouchEffect precisa ser adicionado à Effects coleção do pai da SKCanvasView derivada, para que isso seja feito na OnParentSet substituição:

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

Os eventos de toque processados pelo TouchAction manipulador estão em unidades independentes do dispositivo. Primeiro, eles precisam ser convertidos em pixels usando o ConvertToPixel método na parte inferior da classe e, em seguida, convertidos em CroppingRectangle unidades usando inverseBitmapMatrix.

Para Pressed eventos, o TouchAction manipulador chama o HitTest método de CroppingRectangle. Se isso retornar um índice diferente de –1, um dos cantos do retângulo de corte está sendo manipulado. Esse índice e um deslocamento do ponto de toque real do canto são armazenados em um TouchPoint objeto e adicionados ao touchPoints dicionário.

Para o Moved evento, o MoveCorner método de CroppingRectangle é chamado para mover o canto, com possíveis ajustes para a proporção.

A qualquer momento, um programa que usa PhotoCropperCanvasView pode acessar a CroppedBitmap propriedade. Essa propriedade usa a Rect CroppingRectangle propriedade do para criar um novo bitmap do tamanho cortado. A versão de com retângulos de DrawBitmap destino e origem extrai um subconjunto do bitmap original:

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

Hospedando a visualização da tela do cortador de fotos

Com essas duas classes lidando com a lógica de corte, a página Corte de Foto no aplicativo de exemplo tem muito pouco trabalho a fazer. O arquivo XAML instancia um Grid para hospedar o PhotoCropperCanvasView e um botão Concluído :

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

O PhotoCropperCanvasView não pode ser instanciado no arquivo XAML porque requer um parâmetro do tipo SKBitmap.

Em vez disso, o PhotoCropperCanvasView é instanciado no construtor do arquivo code-behind usando um dos bitmaps de recurso:

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

O usuário pode então manipular o retângulo de corte:

Cortador de fotos 1

Quando um bom retângulo de corte tiver sido definido, clique no botão Concluído . O Clicked manipulador obtém o bitmap cortado da CroppedBitmap propriedade de PhotoCropperCanvasView, e substitui todo o conteúdo da página por um novo SKCanvasView objeto que exibe esse bitmap cortado:

Cortador de fotos 2

Tente definir o segundo argumento de PhotoCropperCanvasView como 1,78f (por exemplo):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Você verá o retângulo de corte restrito a uma proporção de 16 para 9 característica da televisão de alta definição.

Dividindo um bitmap em blocos

Uma Xamarin.Forms versão do famoso quebra-cabeça 14-15 apareceu no Capítulo 22 do livro Criando Aplicativos Móveis com Xamarin.Formse pode ser baixada como XamagonXuzzle. No entanto, o quebra-cabeça se torna mais divertido (e muitas vezes mais desafiador) quando é baseado em uma imagem de sua própria biblioteca de fotos.

Esta versão do quebra-cabeça 14-15 faz parte do aplicativo de exemplo e consiste em uma série de páginas intituladas Photo Puzzle.

O arquivo PhotoPuzzlePage1.xaml consiste em um 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>

O arquivo code-behind implementa um Clicked manipulador que usa o IPhotoLibrary serviço de dependência para permitir que o usuário escolha uma foto da biblioteca de fotos:

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

Em seguida, o método navega até PhotoPuzzlePage2, passando para o construtor o bitmap selecionado.

É possível que a foto selecionada na biblioteca não esteja orientada como aparecia na fototeca, mas esteja girada ou de cabeça para baixo. (Isso é particularmente um problema com dispositivos iOS.) Por esse motivo, PhotoPuzzlePage2 permite girar a imagem para a orientação desejada. O arquivo XAML contém três botões rotulados 90° Direita (ou seja, sentido horário), 90° Esquerda (sentido anti-horário) e Concluído.

O arquivo code-behind implementa a lógica de rotação de bitmap mostrada no artigo Criando e desenhando em bitmaps do SkiaSharp. O usuário pode girar a imagem 90 graus no sentido horário ou anti-horário quantas vezes quiser:

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 o usuário clica no botão Concluído , o Clicked manipulador navega até PhotoPuzzlePage3, passando o bitmap girado final no construtor da página.

PhotoPuzzlePage3 permite que a foto seja cortada. O programa requer um bitmap quadrado para dividir em uma grade de blocos de 4 por 4.

O arquivo PhotoPuzzlePage3.xaml contém um Label, um Grid para hospedar o PhotoCropperCanvasViewe outro botão Concluído :

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

O arquivo code-behind instancia o PhotoCropperCanvasView com o bitmap passado para seu construtor. Observe que um 1 é passado como o segundo argumento para PhotoCropperCanvasView. Essa proporção de 1 força o retângulo de corte a ser um quadrado:

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

O manipulador de botões Concluído obtém a largura e a altura do bitmap cortado (esses dois valores devem ser os mesmos) e, em seguida, divide-o em 15 bitmaps separados, cada um com 1/4 da largura e altura do original. (O último dos 16 bitmaps possíveis não é criado.) O DrawBitmap método com retângulo de origem e destino permite que um bitmap seja criado com base no subconjunto de um bitmap maior.

Convertendo em Xamarin.Forms bitmaps

OnDoneButtonClicked No método, a matriz criada para os 15 bitmaps é do tipo ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource é o Xamarin.Forms tipo base que encapsula um bitmap. Felizmente, o SkiaSharp permite a conversão de bitmaps SkiaSharp para Xamarin.Forms bitmaps. O assembly SkiaSharp.Views.Forms define uma SKBitmapImageSource classe que deriva, ImageSource mas pode ser criada com base em um objeto SkiaSharp SKBitmap . SKBitmapImageSource até define conversões entre SKBitmapImageSource e SKBitmap, e é assim SKBitmap que os objetos são armazenados em uma matriz como Xamarin.Forms bitmaps:

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

Essa matriz de bitmaps é passada como um construtor para o PhotoPuzzlePage4. Essa página é inteiramente Xamarin.Forms e não usa nenhum SkiaSharp. É muito semelhante ao XamagonXuzzle, por isso não será descrito aqui, mas exibe sua foto selecionada dividida em 15 blocos quadrados:

Quebra-cabeça de fotos 1

Pressionar o botão Randomizar mistura todas as peças:

Quebra-cabeça de fotos 2

Agora você pode colocá-los de volta na ordem correta. Quaisquer blocos na mesma linha ou coluna que o quadrado em branco podem ser tocados para movê-los para o quadrado em branco.