Partager via


Rognage des bitmaps SkiaSharp

L’article Création et dessin des bitmaps SkiaSharp décrit comment un SKBitmap objet peut être passé à un SKCanvas constructeur. Toute méthode de dessin appelée sur ce canevas entraîne le rendu des graphiques sur la bitmap. Ces méthodes de dessin incluent DrawBitmap, ce qui signifie que cette technique permet de transférer une partie ou l’ensemble d’une bitmap vers une autre bitmap, peut-être avec des transformations appliquées.

Vous pouvez utiliser cette technique pour rogner une bitmap en appelant la DrawBitmap méthode avec des rectangles source et de destination :

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Toutefois, les applications qui implémentent le rognage fournissent souvent une interface permettant à l’utilisateur de sélectionner de manière interactive le rectangle de rognage :

Exemple de rognage

Cet article se concentre sur cette interface.

Encapsuler le rectangle de rognage

Il est utile d’isoler une partie de la logique de rognage dans une classe nommée CroppingRectangle. Les paramètres du constructeur incluent un rectangle maximal, qui est généralement la taille de l’image bitmap rognée et un rapport d’aspect facultatif. Le constructeur définit d’abord un rectangle de rognage initial, qu’il rend public dans la Rect propriété de type SKRect. Ce rectangle de rognage initial est de 80 % de la largeur et de la hauteur du rectangle bitmap, mais il est ensuite ajusté si un rapport d’aspect est spécifié :

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

Une information utile qui CroppingRectangle rend également disponible est un tableau de SKPoint valeurs correspondant aux quatre coins du rectangle de rognage dans l’ordre supérieur gauche, supérieur droit, inférieur droit et inférieur gauche :

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

Ce tableau est utilisé dans la méthode suivante, qui est appelée HitTest. Le SKPoint paramètre est un point correspondant à un doigt tactile ou à un clic de souris. La méthode retourne un index (0, 1, 2 ou 3) correspondant au coin auquel le doigt ou le pointeur de la souris a touché, à l’intérieur d’une distance donnée par le radius paramètre :

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 le point tactile ou de la souris n’était pas dans radius les unités d’un coin, la méthode retourne –1.

La méthode finale est CroppingRectangle appelée MoveCorner, qui est appelée en réponse au mouvement tactile ou à la souris. Les deux paramètres indiquent l’index du coin déplacé et le nouvel emplacement de ce coin. La première moitié de la méthode ajuste le rectangle de rognage en fonction du nouvel emplacement du coin, mais toujours dans les limites de maxRect, qui est la taille de la bitmap. Cette logique prend également en compte le MINIMUM champ pour éviter de réduire le rectangle de rognage en rien :

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 deuxième moitié de la méthode s’ajuste pour le rapport d’aspect facultatif.

N’oubliez pas que tout ce qui se trouve dans cette classe est en unités de pixels.

Vue canevas uniquement pour rogner

La CroppingRectangle classe que vous venez de voir est utilisée par la PhotoCropperCanvasView classe, qui dérive de SKCanvasView. Cette classe est chargée d’afficher la bitmap et le rectangle de rognage, ainsi que de gérer les événements tactiles ou de souris pour modifier le rectangle de rognage.

Le PhotoCropperCanvasView constructeur nécessite une bitmap. Un rapport d’aspect est facultatif. Le constructeur instancie un objet de type CroppingRectangle basé sur ce rapport bitmap et d’aspect et l’enregistre sous forme de champ :

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

Étant donné que cette classe dérive de SKCanvasView, elle n’a pas besoin d’installer un gestionnaire pour l’événement PaintSurface . Il peut remplacer sa méthode à la OnPaintSurface place. La méthode affiche l’image bitmap et utilise quelques SKPaint objets enregistrés en tant que champs pour dessiner le rectangle de rognage actuel :

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

Le code de la CroppingRectangle classe base le rectangle de rognage sur la taille de pixels de la bitmap. Toutefois, l’affichage de la bitmap par la PhotoCropperCanvasView classe est mis à l’échelle en fonction de la taille de la zone d’affichage. Calculé bitmapScaleMatrix dans les OnPaintSurface mappages de remplacement des pixels bitmap à la taille et à la position de la bitmap tel qu’il est affiché. Cette matrice est ensuite utilisée pour transformer le rectangle de rognage afin qu’il puisse être affiché par rapport à la bitmap.

La dernière ligne du OnPaintSurface remplacement prend l’inverse du bitmapScaleMatrix champ et l’enregistre en tant que inverseBitmapMatrix champ. Ceci est utilisé pour le traitement tactile.

Un TouchEffect objet est instancié en tant que champ et le constructeur attache un gestionnaire à l’événement TouchAction , mais il TouchEffect doit être ajouté à la Effects collection du parent de la SKCanvasView dérivée, de sorte que cela soit effectué dans le OnParentSet remplacement :

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

Les événements tactiles traités par le gestionnaire se trouvent dans des unités indépendantes de l’appareil TouchAction . Elles doivent d’abord être converties en pixels à l’aide de la ConvertToPixel méthode en bas de la classe, puis converties en CroppingRectangle unités à l’aide inverseBitmapMatrixde .

Pour Pressed les événements, le TouchAction gestionnaire appelle la HitTest méthode de CroppingRectangle. Si cela retourne un index autre que –1, l’un des angles du rectangle de rognage est en cours de manipulation. Cet index et un décalage du point tactile réel à partir du coin sont stockés dans un TouchPoint objet et ajoutés au touchPoints dictionnaire.

Pour l’événement Moved , la MoveCorner méthode d’est CroppingRectangle appelée pour déplacer le coin, avec des ajustements possibles pour le rapport d’aspect.

À tout moment, un programme qui utilise PhotoCropperCanvasView peut accéder à la CroppedBitmap propriété. Cette propriété utilise la Rect propriété de la CroppingRectangle propriété pour créer une image bitmap de la taille rognée. La version des rectangles de destination et de DrawBitmap source extrait ensuite un sous-ensemble de la bitmap d’origine :

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

Hébergement de la vue canevas du rognage photo

Avec ces deux classes qui gèrent la logique de rognage, la page Rognage de photo dans l’exemple d’application a très peu de travail à faire. Le fichier XAML instancie un Grid pour héberger le PhotoCropperCanvasView bouton Terminé et l’héberger :

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

Impossible PhotoCropperCanvasView d’instancier le fichier XAML, car il nécessite un paramètre de type SKBitmap.

Au lieu de cela, il PhotoCropperCanvasView est instancié dans le constructeur du fichier code-behind à l’aide de l’une des bitmaps de ressource :

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’utilisateur peut ensuite manipuler le rectangle de rognage :

Rognage photo 1

Lorsqu’un bon rectangle de rognage a été défini, cliquez sur le bouton Terminé . Le Clicked gestionnaire obtient la bitmap rognée à partir de la CroppedBitmap propriété de PhotoCropperCanvasView, et remplace tout le contenu de la page par un nouvel SKCanvasView objet qui affiche cette bitmap rognée :

Rognage photo 2

Essayez de définir le deuxième argument de PhotoCropperCanvasView la valeur 1.78f (par exemple) :

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Vous verrez le rectangle de rognage limité à une caractéristique d’aspect de 16 à 9 caractéristiques de la télévision haute définition.

Division d’une bitmap en vignettes

Une Xamarin.Forms version du célèbre puzzle 14-15 est apparue dans le chapitre 22 du livre Creating Mobile Apps with Xamarin.Formsand can be downloaded as XamagonXuzzle. Toutefois, le puzzle devient plus amusant (et souvent plus difficile) quand il est basé sur une image de votre propre bibliothèque de photos.

Cette version du puzzle 14-15 fait partie de l’exemple d’application et se compose d’une série de pages intitulées Photo Puzzle.

Le fichier PhotoPuzzlePage1.xaml se compose d’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>

Le fichier code-behind implémente un Clicked gestionnaire qui utilise le IPhotoLibrary service de dépendances pour permettre à l’utilisateur de choisir une photo à partir de la bibliothèque de photos :

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

La méthode accède ensuite à PhotoPuzzlePage2, en passant au constuctor l’image bitmap sélectionnée.

Il est possible que la photo sélectionnée à partir de la bibliothèque ne soit pas orientée comme elle apparaît dans la bibliothèque de photos, mais qu’elle est pivotée ou à l’envers. (Il s’agit en particulier d’un problème avec les appareils iOS.) Pour cette raison, PhotoPuzzlePage2 vous pouvez faire pivoter l’image vers une orientation souhaitée. Le fichier XAML contient trois boutons étiquetés 90° Droite (sens sens de l’horloge), 90° Gauche (dans le sens inverse) et Terminé.

Le fichier code-behind implémente la logique de rotation bitmap indiquée dans l’article Création et dessin sur les bitmaps SkiaSharp. L’utilisateur peut faire pivoter l’image de 90 degrés dans le sens des aiguilles d’une montre ou dans le sens inverse des aiguilles d’une montre n’importe quel nombre de fois :

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

Lorsque l’utilisateur clique sur le bouton Terminé , le Clicked gestionnaire accède à PhotoPuzzlePage3, en passant la bitmap pivotée finale dans le constructeur de la page.

PhotoPuzzlePage3 permet à la photo d’être rognée. Le programme nécessite une bitmap carrée pour se diviser en une grille de 4 à 4 mosaïques.

Le fichier PhotoPuzzlePage3.xaml contient un Label, un Grid pour héberger le PhotoCropperCanvasViewfichier et un autre bouton Terminé :

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

Le fichier code-behind instancie l’image PhotoCropperCanvasView bitmap passée à son constructeur. Notez qu’un 1 est passé en tant que deuxième argument à PhotoCropperCanvasView. Ce rapport d’aspect de 1 force le rectangle de rognage à être un carré :

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

Le gestionnaire de boutons Terminé obtient la largeur et la hauteur de l’image bitmap rognée (ces deux valeurs doivent être identiques), puis le divise en 15 bitmaps distinctes, chacune étant 1/4 la largeur et la hauteur de l’original. (Le dernier des 16 bitmaps possibles n’est pas créé.) La DrawBitmap méthode avec un rectangle source et de destination permet de créer une bitmap en fonction du sous-ensemble d’une image bitmap plus grande.

Conversion en Xamarin.Forms bitmaps

Dans la OnDoneButtonClicked méthode, le tableau créé pour les 15 bitmaps est de type ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource est le Xamarin.Forms type de base qui encapsule une bitmap. Heureusement, SkiaSharp permet de convertir des bitmaps SkiaSharp en Xamarin.Forms bitmaps. L’assembly SkiaSharp.Views.Forms définit une SKBitmapImageSource classe qui dérive, ImageSource mais qui peut être créée en fonction d’un objet SkiaSharpSKBitmap. SKBitmapImageSource définit même les conversions entre SKBitmapImageSource et SKBitmap, et c’est ainsi SKBitmap que les objets sont stockés dans un tableau en tant que Xamarin.Forms bitmaps :

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

Ce tableau de bitmaps est passé en tant que constructeur à PhotoPuzzlePage4. Cette page est entièrement Xamarin.Forms et n’utilise aucune SkiaSharp. Il est très similaire à XamagonXuzzle, donc il ne sera pas décrit ici, mais il affiche votre photo sélectionnée divisée en 15 mosaïques carrées :

Puzzle photo 1

Appuyez sur le bouton Aléatoire pour mélanger toutes les vignettes :

Puzzle photo 2

Maintenant, vous pouvez les remettre dans l’ordre correct. Toutes les vignettes de la même ligne ou colonne que le carré vide peuvent être tapées pour les déplacer dans le carré vide.