Recorte de mapas de bits de SkiaSharp

Download SampleDescargar el ejemplo

En el artículo Creación y dibujo de mapas de bits de SkiaSharp se describe cómo se puede pasar un objetoSKBitmap a un constructor SKCanvas. Cualquier método de dibujo al que se llama en ese lienzo hace que los gráficos se representen en el mapa de bits. Estos métodos de dibujo incluyen DrawBitmap, lo que significa que esta técnica permite transferir parte o todo un mapa de bits a otro mapa de bits, quizás con transformaciones aplicadas.

Puede usar esa técnica para recortar un mapa de bits llamando al método DrawBitmap con rectángulos de origen y destino:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Sin embargo, las aplicaciones que implementan el recorte suelen proporcionar una interfaz para que el usuario seleccione interactivamente el rectángulo de recorte:

Cropping Sample

Este artículo se centra en esa interfaz.

Encapsular el rectángulo de recorte

Resulta útil aislar parte de la lógica de recorte en una clase denominada CroppingRectangle. Los parámetros del constructor incluyen un rectángulo máximo, que suele ser el tamaño del mapa de bits que se recorta y una relación de aspecto opcional. El constructor define primero un rectángulo de recorte inicial, que hace público en la propiedad Rect de tipo SKRect. Este rectángulo de recorte inicial es el 80 % del ancho y alto del rectángulo de mapa de bits, pero se ajusta después si se especifica una relación de aspecto:

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 información útil que CroppingRectangle también pone a disposición es una matriz de valores SKPoint correspondientes a las cuatro esquinas del rectángulo de recorte en el orden superior izquierdo, superior derecha, inferior derecha e inferior izquierda:

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

Esta matriz se usa en el método siguiente, que se denomina HitTest. El parámetro SKPoint es un punto correspondiente a un toque o un clic del mouse. El método devuelve un índice (0, 1, 2 o 3) correspondiente a la esquina que tocó el dedo o el puntero del mouse, a una distancia dada por el parámetro radius:

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

Si el punto táctil o del mouse no estaba dentro de unidades radius de ninguna esquina, el método devuelve –1.

El método final de CroppingRectangle se denomina MoveCorner, que se llama en respuesta al movimiento táctil o del mouse. Los dos parámetros indican el índice de la esquina que se mueve y la nueva ubicación de esa esquina. La primera mitad del método ajusta el rectángulo de recorte en función de la nueva ubicación de la esquina, pero siempre dentro de los límites de maxRect, que es el tamaño del mapa de bits. Esta lógica también tiene en cuenta el campo MINIMUM para evitar contraer el rectángulo de recorte a 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;
    }
}

La segunda mitad del método se ajusta para la relación de aspecto opcional.

Tenga en cuenta que todo lo de esta clase está en unidades de píxeles.

Una vista depanel de lienzo solo para recortar

La CroppingRectangle clase que acaba de ver se usa en la clase PhotoCropperCanvasView, que deriva de SKCanvasView. Esta clase es responsable de mostrar el mapa de bits y el rectángulo de recorte, así como controlar los eventos táctiles o del mouse para cambiar el rectángulo de recorte.

El constructor PhotoCropperCanvasView requiere un mapa de bits. Una relación de aspecto es opcional. El constructor crea una instancia de un objeto de tipo CroppingRectangle, basado en este mapa de bits y relación de aspecto, y lo guarda como un 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);
        ···
    }
    ···
}

Dado que esta clase deriva de SKCanvasView, no es necesario instalar un controlador para el evento PaintSurface. En su lugar, puede invalidar su método OnPaintSurface. El método muestra el mapa de bits y usa un par de objetosSKPaint guardados como campos para dibujar el rectángulo de recorte actual:

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

El código de la clase CroppingRectangle basa el rectángulo de recorte en el tamaño de píxel del mapa de bits. Sin embargo, la presentación del mapa de bits por la clase PhotoCropperCanvasView se escala en función del tamaño del área de presentación. El objeto bitmapScaleMatrix calculado en la invalidación OnPaintSurface se asigna desde los píxeles de mapa de bits hasta el tamaño y la posición del mapa de bits tal como se muestra. A continuación, esta matriz se usa para transformar el rectángulo de recorte para que se pueda mostrar en relación con el mapa de bits.

La última línea de la invalidación OnPaintSurface toma el inverso de bitmapScaleMatrix y lo guarda como campo inverseBitmapMatrix. Esto se usa para el procesamiento táctil.

Se crea una instancia de un objeto TouchEffect como campo y el constructor adjunta un controlador al evento TouchAction, pero debe agregarse TouchEffect a la colección Effects del elemento primario del derivado SKCanvasView, de modo que se realice en la invalidación 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));
    }
}

Los eventos táctiles procesados por el controlador se encuentran en unidades independientes del dispositivo TouchAction. Estos primero deben convertirse en píxeles mediante el método ConvertToPixel situado en la parte inferior de la clase y, a continuación, convertirse en unidades CroppingRectangle mediante inverseBitmapMatrix.

En el caso de los eventos Pressed, el controlador TouchAction llama al método HitTest de CroppingRectangle. Si devuelve un índice distinto de –1, se está manipulando una de las esquinas del rectángulo de recorte. Ese índice y un desplazamiento del punto táctil real de la esquina se almacenan en un objeto TouchPoint y se agregan al diccionario touchPoints.

Para el evento Moved, se llama al método MoveCorner de CroppingRectangle para mover la esquina, con posibles ajustes para la relación de aspecto.

En cualquier momento, un programa mediante PhotoCropperCanvasView puede acceder a la propiedad CroppedBitmap. Esta propiedad usa la propiedad Rect de CroppingRectangle para crear un mapa de bits nuevo del tamaño recortado. A continuación, la versión de con rectángulos de DrawBitmap con destino y origen extrae un subconjunto del mapa de bits 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;
        }
    }
    ···
}

Hospedaje de la vista de lienzo del recortador de fotos

Con esas dos clases que controlan la lógica de recorte, la página Recorte de fotos en la aplicación SkiaSharpFormsDemos tiene muy poco trabajo que hacer. El archivo XAML crea una instancia Grid para hospedar el PhotoCropperCanvasView y un botón de Listo:

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

PhotoCropperCanvasView no puede crear una instancia en el archivo XAML porque requiere un parámetro de tipo SKBitmap.

En su lugar, el PhotoCropperCanvasView crea una instancia en el constructor del archivo de código subyacente mediante uno de los mapas de bits de recursos:

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

El usuario puede entonces manipular el rectángulo de recorte:

Photo Cropper 1

Cuando se haya definido un buen rectángulo de recorte, haga clic en el botón Listo. El controlador Clicked obtiene el mapa de bits recortado de la propiedad CroppedBitmap de PhotoCropperCanvasView y reemplaza todo el contenido de la página por un nuevo objeto SKCanvasView que muestra este mapa de bits recortado:

Photo Cropper 2

Intente establecer el segundo argumento de PhotoCropperCanvasView en 1.78f (por ejemplo):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Verá el rectángulo de recorte restringido a una relación de aspecto de 16 a 9 de televisión de alta definición.

Dividir un mapa de bits en mosaicos

Una versión Xamarin.Forms del famoso rompecabezas 14-15 apareció en el capítulo 22 del libro X Creación de aplicaciones móviles con Xamarin.Forms y se puede descargar como XamagonXuzzle. Sin embargo, el rompecabezas se vuelve más divertido (y a menudo más difícil) cuando se basa en una imagen de su propia biblioteca de fotos.

Esta versión del rompecabezas 14-15 forma parte de la aplicación SkiaSharpFormsDemos y consta de una serie de páginas tituladas Photo Puzzle.

El archivo PhotoPuzzlePage1.xaml consta de un 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>

El archivo de código subyacente implementa un controlador Clicked que usa el servicio IPhotoLibrary de dependencias para permitir al usuario elegir una foto de la 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));
            }
        }
    }
}

El método navega entonces a PhotoPuzzlePage2, pasando al constructor el mapa de bits seleccionado.

Es posible que la foto seleccionada en la biblioteca no esté orientada como apareció en la biblioteca de fotos, pero está girado o al revés. (Esto es especialmente un problema con dispositivos iOS). Por ese motivo, PhotoPuzzlePage2 permite girar la imagen a la orientación deseada. El archivo XAML contiene tres botones etiquetados como 90° a la derecha (lo que significa en el sentido de las agujas del reloj), 90° a la izquierda (en sentido contrario a las agujas del reloj) y Listo.

El archivo de código subyacente implementa la lógica de rotación de mapa de bits que se muestra en el artículo Creación y dibujo en mapas de bits SkiaSharp. El usuario puede girar la imagen 90 grados en el sentido de las agujas del reloj o en sentido contrario tantas veces como quiera:

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

Cuando el usuario hace clic en el botón Listo, el controlador Clicked navega a PhotoPuzzlePage3, pasando el mapa de bits girado final en el constructor de la página.

PhotoPuzzlePage3 permite recortar la foto. El programa requiere un mapa de bits cuadrado para dividir en una cuadrícula de mosaicos de 4 por 4.

El archivo PhotoPuzzlePage3.xaml contiene un Label, un Grid para hospedar el PhotoCropperCanvasViewy otro botónListo:

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

El archivo de código subyacente crea una instancia del PhotoCropperCanvasView objeto con el mapa de bits pasado a su constructor. Observe que se pasa un 1 como segundo argumento a PhotoCropperCanvasView. Esta relación de aspecto de 1 obliga al rectángulo de recorte a ser un cuadrado:

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

El controlador del botón Listo obtiene el ancho y alto del mapa de bits recortado (estos dos valores deben ser los mismos) y después lo divide en 15 mapas de bits independientes, cada uno de los cuales es 1/4 el ancho y el alto del original. (No se crea el último de los posibles mapas de bits de 16). El método DrawBitmap con rectángulo de origen y destino permite crear un mapa de bits basado en un subconjunto de un mapa de bits mayor.

Conversión a mapas de bits Xamarin.Forms

En el método OnDoneButtonClicked la matriz creada para los mapas de bits de 15 es de tipo ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource es el tipo base Xamarin.Forms que encapsula un mapa de bits. Afortunadamente, SkiaSharp permite la conversión de mapas de bits de SkiaSharp a mapas de bits Xamarin.Forms. El ensamblado SkiaSharp.Views.Forms define una clase SKBitmapImageSource que deriva de ImageSource pero que se puede crear en función de un objeto SkiaSharp SKBitmap. SKBitmapImageSource incluso define conversiones entre SKBitmapImageSource y SKBitmap, y es cómo se almacenan los objetos SKBitmap en una matriz como Xamarin.Forms mapas de bits:

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

Esta matriz de mapas de bits se pasa como constructor a PhotoPuzzlePage4. Esa página es completamente Xamarin.Forms y no usa SkiaSharp. Es muy similar a XamagonXuzzle, por lo que no se describirá aquí, pero muestra la foto seleccionada dividida en 15 mosaicos cuadrados:

Photo Puzzle 1

Al presionar el botón Random (Aleatorio) se mezclan todos los iconos:

Photo Puzzle 2

Ahora puede volver a ponerlos en el orden correcto. Los elementos de la misma fila o columna que el cuadrado en blanco se pueden pulsar para moverlos al cuadrado en blanco.