Zuschneiden von SkiaSharp-Bitmaps
Im Artikel Erstellen und Zeichnen von SkiaSharp-Bitmaps wurde beschrieben, wie ein SKBitmap
Objekt an einen SKCanvas
Konstruktor übergeben werden kann. Jede Zeichnungsmethode, die für diesen Zeichenbereich aufgerufen wird, bewirkt, dass Grafiken in der Bitmap gerendert werden. Diese Zeichnungsmethoden umfassen DrawBitmap
, was bedeutet, dass diese Technik das Übertragen eines Teils oder der gesamten Bitmap in eine andere Bitmap ermöglicht, möglicherweise mit angewendeten Transformationen.
Sie können diese Technik zum Zuschneiden einer Bitmap verwenden, indem Sie die DrawBitmap
-Methode mit Quell- und Zielrechtecken aufrufen:
canvas.DrawBitmap(bitmap, sourceRect, destRect);
Anwendungen, die das Zuschneiden implementieren, bieten jedoch häufig eine Schnittstelle, über die der Benutzer das Zuschneiderechteck interaktiv auswählen kann:
Dieser Artikel konzentriert sich auf diese Schnittstelle.
Kapseln des Zuschneiderechtecks
Es ist hilfreich, einen Teil der Zuschneidelogik in einer Klasse namens CroppingRectangle
zu isolieren. Die Konstruktorparameter umfassen ein maximales Rechteck, das in der Regel die Größe der zuschneidenden Bitmap ist, und ein optionales Seitenverhältnis. Der Konstruktor definiert zuerst ein anfängliches Zuschneiderechteck, das er in der Rect
-Eigenschaft des Typs SKRect
öffentlich macht. Dieses anfängliche Zuschneiderechteck beträgt 80 % der Breite und Höhe des Bitmaprechtecks, wird jedoch angepasst, wenn ein Seitenverhältnis angegeben wird:
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; }
···
}
Eine nützliche Information, die ebenfalls verfügbar macht, CroppingRectangle
ist ein Array von SKPoint
Werten, die den vier Ecken des Zuschneiderechtecks in der Reihenfolge oben links, oben rechts, unten rechts und unten links entsprechen:
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)
};
}
}
···
}
Dieses Array wird in der folgenden Methode verwendet, die als bezeichnet HitTest
wird. Der SKPoint
Parameter ist ein Punkt, der einer Fingereingabe oder einem Mausklick entspricht. Die -Methode gibt einen Index (0, 1, 2 oder 3) zurück, der der Ecke entspricht, die der Finger- oder Mauszeiger berührt hat, innerhalb einer entfernung, die radius
vom Parameter angegeben wird:
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;
}
···
}
Wenn sich der Touch- oder Mauspunkt nicht innerhalb radius
der Einheiten einer Ecke befand, gibt die Methode –1 zurück.
Die letzte Methode in CroppingRectangle
heißt MoveCorner
, die als Reaktion auf Touch- oder Mausbewegungen aufgerufen wird. Die beiden Parameter geben den Index der zu verschiebenden Ecke und die neue Position dieser Ecke an. Die erste Hälfte der Methode passt das Zuschneiderechteck basierend auf der neuen Position der Ecke an, jedoch immer innerhalb der Grenzen von maxRect
, was der Größe der Bitmap entspricht. Diese Logik berücksichtigt auch das MINIMUM
Feld, um zu vermeiden, dass das Zuschneiderechteck in nichts zusammenbricht:
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;
}
}
Die zweite Hälfte der Methode wird für das optionale Seitenverhältnis angepasst.
Beachten Sie, dass sich alles in dieser Klasse in Pixeleinheiten befindet.
Eine Canvasansicht nur zum Zuschneiden
Die CroppingRectangle
soeben gesehene Klasse wird von der PhotoCropperCanvasView
-Klasse verwendet, die von SKCanvasView
abgeleitet wird. Diese Klasse ist für die Anzeige der Bitmap und des Zuschneiderechtecks sowie für die Behandlung von Touch- oder Mausereignissen zum Ändern des Zuschneiderechtecks zuständig.
Der PhotoCropperCanvasView
Konstruktor erfordert eine Bitmap. Ein Seitenverhältnis ist optional. Der Konstruktor instanziiert ein Objekt vom Typ CroppingRectangle
basierend auf diesem Bitmap- und Seitenverhältnis und speichert es als Feld:
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);
···
}
···
}
Da diese Klasse von SKCanvasView
abgeleitet ist, muss kein Handler für das PaintSurface
-Ereignis installiert werden. Stattdessen kann die -Methode überschrieben werden OnPaintSurface
. Die -Methode zeigt die Bitmap an und verwendet einige objekte SKPaint
, die als Felder gespeichert sind, um das aktuelle Zuschneiderechteck zu zeichnen:
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);
}
···
}
Der Code in der CroppingRectangle
-Klasse basiert das Zuschneiderechteck auf der Pixelgröße der Bitmap. Die Anzeige der Bitmap durch die PhotoCropperCanvasView
-Klasse wird jedoch basierend auf der Größe des Anzeigebereichs skaliert. Die bitmapScaleMatrix
in der OnPaintSurface
Überschreibung berechnete ordnet die Bitmappixel der Größe und Position der Bitmap zu, während sie angezeigt wird. Diese Matrix wird dann verwendet, um das Zuschneiderechteck so zu transformieren, dass es relativ zur Bitmap angezeigt werden kann.
Die letzte Zeile der OnPaintSurface
Außerkraftsetzung nimmt die Umgekehrte von bitmapScaleMatrix
und speichert sie als Feld inverseBitmapMatrix
. Dies wird für die Verarbeitung von Toucheingaben verwendet.
Ein TouchEffect
-Objekt wird als Feld instanziiert, und der Konstruktor fügt einen Handler an das TouchAction
-Ereignis an, aber das TouchEffect
muss der Effects
Auflistung des übergeordnetenSKCanvasView
Derivat hinzugefügt werden, damit dies in der OnParentSet
Außerkraftsetzung erfolgt:
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));
}
}
Die vom TouchAction
Handler verarbeiteten Touchereignisse befinden sich in geräteunabhängigen Einheiten. Diese müssen zunächst mithilfe der ConvertToPixel
-Methode am unteren Rand der -Klasse in Pixel konvertiert und dann mithilfe inverseBitmapMatrix
von in Einheiten konvertiert CroppingRectangle
werden.
Für Pressed
Ereignisse ruft der TouchAction
Handler die HitTest
-Methode von auf CroppingRectangle
. Wenn dadurch ein anderer Index als –1 zurückgegeben wird, wird eine der Ecken des Zuschneiderechtecks bearbeitet. Dieser Index und ein Offset des tatsächlichen Berührungspunkts von der Ecke werden in einem TouchPoint
-Objekt gespeichert und dem touchPoints
Wörterbuch hinzugefügt.
Für das Moved
-Ereignis wird die MoveCorner
-Methode von CroppingRectangle
aufgerufen, um die Ecke mit möglichen Anpassungen für das Seitenverhältnis zu verschieben.
Ein Programm, das verwendet PhotoCropperCanvasView
, kann jederzeit auf die CroppedBitmap
-Eigenschaft zugreifen. Diese Eigenschaft verwendet die Rect
-Eigenschaft von CroppingRectangle
, um eine neue Bitmap der zugeschnittenen Größe zu erstellen. Die Version von DrawBitmap
mit Ziel- und Quellrechtecke extrahiert dann eine Teilmenge der ursprünglichen Bitmap:
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;
}
}
···
}
Hosten der Fotozuschneidebereichsansicht
Da diese beiden Klassen die Zuschneidelogik behandeln, hat die Seite Fotozuschneidevorgänge in der Anwendung SkiaSharpFormsDemos nur sehr wenig Arbeit zu erledigen. Die XAML-Datei instanziiert eine Grid
, um die PhotoCropperCanvasView
Schaltflächen und die Schaltflächen Fertig zu hosten :
<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>
Kann PhotoCropperCanvasView
in der XAML-Datei nicht instanziiert werden, da ein Parameter vom Typ SKBitmap
erforderlich ist.
Stattdessen wird im PhotoCropperCanvasView
Konstruktor der CodeBehind-Datei mithilfe einer der Ressourcenbits instanziiert:
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);
}
}
Der Benutzer kann dann das Zuschneiderechteck bearbeiten:
Wenn ein gutes Zuschneiderechteck definiert wurde, klicken Sie auf die Schaltfläche Fertig . Der Clicked
Handler ruft die zugeschnittene Bitmap aus der CroppedBitmap
-Eigenschaft von PhotoCropperCanvasView
ab und ersetzt den gesamten Inhalt der Seite durch ein neues SKCanvasView
Objekt, das diese zugeschnittene Bitmap anzeigt:
Versuchen Sie, das zweite Argument von PhotoCropperCanvasView
auf 1,78f festzulegen (z. B.):
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
Sie sehen das Zuschneiderechteck, das auf ein Seitenverhältnis von 16 zu 9 beschränkt ist, das für hochauflösendes Fernsehen charakteristisch ist.
Aufteilen einer Bitmap in Kacheln
Eine Xamarin.Forms Version des berühmten 14-15-Puzzles erschien in Kapitel 22 des Buches Creating Mobile Apps with Xamarin.Forms und kann als XamagonXuzzle heruntergeladen werden. Allerdings wird das Puzzle mehr Spaß (und oft anspruchsvoller), wenn es auf einem Bild aus Ihrer eigenen Fotobibliothek basiert.
Diese Version des 14-15-Puzzles ist Teil der SkiaSharpFormsDemos-Anwendung und besteht aus einer Reihe von Seiten mit dem Titel Photo Puzzle.
Die Datei PhotoPuzzlePage1.xaml besteht aus :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>
Die CodeBehind-Datei implementiert einen Clicked
Handler, der den IPhotoLibrary
Abhängigkeitsdienst verwendet, um dem Benutzer die Auswahl eines Fotos aus der Fotobibliothek zu ermöglichen:
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));
}
}
}
}
Die -Methode navigiert dann zu PhotoPuzzlePage2
, und übergibt an den Constuctor die ausgewählte Bitmap.
Es ist möglich, dass das aus der Bibliothek ausgewählte Foto nicht so ausgerichtet ist, wie es in der Fotobibliothek angezeigt wurde, sondern gedreht oder auf den Kopf gestellt wird. (Dies ist insbesondere bei iOS-Geräten ein Problem.) Aus diesem Grund PhotoPuzzlePage2
können Sie das Bild in eine gewünschte Ausrichtung drehen. Die XAML-Datei enthält drei Schaltflächen mit der Bezeichnung 90° Rechts (d. h. im Uhrzeigersinn), 90° Links (gegen den Uhrzeigersinn) und Fertig.
Die CodeBehind-Datei implementiert die Bitmapdrehungslogik, die im Artikel Erstellen und Zeichnen von SkiaSharp-Bitmaps gezeigt wird. Der Benutzer kann das Bild beliebig oft um 90 Grad im oder gegen den Uhrzeigersinn drehen:
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));
}
}
Wenn der Benutzer auf die Schaltfläche Fertig klickt, navigiert der Clicked
Handler zu PhotoPuzzlePage3
und übergibt die endgültige gedrehte Bitmap im Konstruktor der Seite.
PhotoPuzzlePage3
ermöglicht das Zuschneiden des Fotos. Das Programm erfordert eine quadratische Bitmap, um in ein 4-mal-4-Raster von Kacheln zu unterteilen.
Die Datei PhotoPuzzlePage3.xaml enthält eine Label
, eine Grid
zum Hosten von PhotoCropperCanvasView
und eine weitere Schaltfläche Fertig :
<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>
Die CodeBehind-Datei instanziiert die PhotoCropperCanvasView
mit der An ihren Konstruktor übergebenen Bitmap. Beachten Sie, dass eine 1 als zweites Argument an PhotoCropperCanvasView
übergeben wird. Dieses Seitenverhältnis von 1 erzwingt, dass das Zuschneiderechteck ein Quadrat ist:
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));
}
}
Der Schaltflächenhandler fertig ruft die Breite und Höhe der zugeschnittenen Bitmap ab (diese beiden Werte sollten identisch sein) und teilt sie dann in 15 separate Bitmaps, von denen jedes 1/4 die Breite und Höhe des Originals aufweist. (Die letzte der möglichen 16 Bitmaps wird nicht erstellt.) Die DrawBitmap
-Methode mit Quell- und Zielrechteck ermöglicht das Erstellen einer Bitmap basierend auf einer Teilmenge einer größeren Bitmap.
Konvertieren in Xamarin.Forms Bitmaps
In der OnDoneButtonClicked
-Methode ist das Array, das für die 15 Bitmaps erstellt wurde, vom Typ ImageSource
:
ImageSource[] imgSources = new ImageSource[15];
ImageSource
ist der Xamarin.Forms Basistyp, der eine Bitmap kapselt. Glücklicherweise ermöglicht SkiaSharp die Konvertierung von SkiaSharp-Bitmaps in Xamarin.Forms Bitmaps. Die SkiaSharp.Views.Forms-Assembly definiert eine SKBitmapImageSource
Klasse, die von ImageSource
abgeleitet wird, aber basierend auf einem SkiaSharp-Objekt SKBitmap
erstellt werden kann. SKBitmapImageSource
definiert sogar Konvertierungen zwischen SKBitmapImageSource
und SKBitmap
, und so SKBitmap
werden Objekte in einem Array als Xamarin.Forms Bitmaps gespeichert:
imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
Dieses Array von Bitmaps wird als Konstruktor an PhotoPuzzlePage4
übergeben. Diese Seite ist vollständig Xamarin.Forms und verwendet keine SkiaSharp. Es ist XamagonXuzzle sehr ähnlich, daher wird es hier nicht beschrieben, aber es zeigt Ihr ausgewähltes Foto in 15 quadratische Kacheln unterteilt:
Durch drücken der Schaltfläche Randomize werden alle Kacheln durcheinander gemischt:
Jetzt können Sie sie wieder in der richtigen Reihenfolge platzieren. Alle Kacheln in derselben Zeile oder Spalte wie das leere Quadrat können angetippt werden, um sie in das leere Quadrat zu verschieben.