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 :
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 inverseBitmapMatrix
de .
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 :
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 :
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 PhotoCropperCanvasView
fichier 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 :
Appuyez sur le bouton Aléatoire pour mélanger toutes les vignettes :
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.