Zuschneiden von SkiaSharp-Bitmaps
Im Artikel "Creating and Drawing SkiaSharp Bitmaps " wird 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 Zeichenmethoden enthalten DrawBitmap
, was bedeutet, dass diese Technik das Übertragen eines Teils oder aller Bitmaps zu einer anderen Bitmap ermöglicht, z. B. 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 zuschneiden implementieren, bieten dem Benutzer jedoch häufig eine Benutzeroberfläche, um das Zuschneiderechteck interaktiv auszuwählen:
Dieser Artikel konzentriert sich auf diese Schnittstelle.
Kapselung des Zuschneiderechtecks
Es ist hilfreich, einige der Zuschneidelogik in einer Klasse mit dem Namen CroppingRectangle
zu isolieren. Die Konstruktorparameter enthalten ein maximales Rechteck, das im Allgemeinen die Größe der zuzuschneidenden Bitmap und ein optionales Seitenverhältnis darstellt. Der Konstruktor definiert zuerst ein anfängliches Zuschneiderechteck, das in der Rect
Eigenschaft des Typs SKRect
öffentlich wird. 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; }
···
}
Ein nützliches Informationselement, das auch zur Verfügung stellt, 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 aufgerufen 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 eines Abstands des radius
Parameters:
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 Fingereingabe- oder Mauspunkt nicht innerhalb radius
einer Ecke befindet, gibt die Methode "-1" zurück.
Die letzte Methode CroppingRectangle
wird aufgerufen 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, aber immer innerhalb der Grenzen maxRect
der Bitmap an. Diese Logik berücksichtigt auch das MINIMUM
Feld, um zu vermeiden, dass das Zuschneiderechteck in nichts unterteilt wird:
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 passt sich für das optionale Seitenverhältnis an.
Denken Sie daran, dass sich alles in dieser Klasse in Pixeleinheiten befindet.
Eine Canvasansicht nur zum Zuschneiden
Die CroppingRectangle
gerade gesehene Klasse wird von der PhotoCropperCanvasView
Klasse verwendet, die von SKCanvasView
. 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 verantwortlich.
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. Es kann stattdessen seine OnPaintSurface
Methode überschreiben. Die Methode zeigt die Bitmap an und verwendet einige SKPaint
Objekte, 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 auf dem 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
Außerkraftsetzung berechnete Zuordnung von Bitmappixeln zur Größe und Position der Bitmap, wie sie angezeigt wird. Diese Matrix wird dann verwendet, um das Zuschneiderechteck zu transformieren, sodass es relativ zur Bitmap angezeigt werden kann.
Die letzte Zeile der OnPaintSurface
Überschreibung übernimmt die Umkehrung des bitmapScaleMatrix
Felds und speichert sie als inverseBitmapMatrix
Feld. Dies wird für die Touchverarbeitung verwendet.
Ein TouchEffect
Objekt wird als Feld instanziiert, und der Konstruktor fügt einen Handler an das TouchAction
Ereignis an, muss jedoch TouchEffect
der Effects
Auflistung des übergeordneten Elements des SKCanvasView
Abgeleiteten hinzugefügt werden, sodass 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 zuerst mithilfe der ConvertToPixel
Methode am unteren Rand der Klasse in Pixel konvertiert und dann mithilfe inverseBitmapMatrix
von Einheiten konvertiert CroppingRectangle
werden.
Für Pressed
Ereignisse ruft der TouchAction
Handler die HitTest
Methode von CroppingRectangle
. Wenn dieser einen anderen Index als -1 zurückgibt, 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 CroppingRectangle
aufgerufen, um die Ecke zu verschieben, mit möglichen Anpassungen für das Seitenverhältnis.
Jederzeit kann ein Programm, das PhotoCropperCanvasView
verwendet wird, auf die CroppedBitmap
Eigenschaft zugreifen. Diese Eigenschaft verwendet die Rect
Eigenschaft der CroppingRectangle
, um eine neue Bitmap der zugeschnittenen Größe zu erstellen. Die Version mit DrawBitmap
Ziel- und Quellrechtecken 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 Fotozuschnitt-Canvasansicht
Mit diesen beiden Klassen, die die Zuschneidelogik behandeln, hat die Seite "Fotozuschneiden " in der Beispielanwendung sehr wenig Arbeit. Die XAML-Datei instanziiert eine Grid
zum Hosten der PhotoCropperCanvasView
Schaltfläche "Fertig" und " Fertig ":
<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>
Die PhotoCropperCanvasView
Instanziierung in der XAML-Datei ist nicht möglich, da ein Parameter vom Typ SKBitmap
erforderlich ist.
Stattdessen wird die PhotoCropperCanvasView
Instanziierung im Konstruktor der CodeBehind-Datei mithilfe einer der Ressourcenbitmaps 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
und ersetzt den gesamten Inhalt der Seite durch ein neues SKCanvasView
Objekt, das diese zugeschnittene Bitmap anzeigt:
Versuchen Sie, das zweite Argument auf PhotoCropperCanvasView
1,78f festzulegen (z. B.):
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
Das zuschneidende Rechteck ist auf ein Seitenverhältnis von 16 bis 9 beschränkt.
Aufteilen einer Bitmap in Kacheln
Eine Xamarin.Forms Version des berühmten 14-15 Puzzles erschien in Kapitel 22 des Buches Creating Mobile Apps mit Xamarin.Formsund kann als XamagonXuzzle heruntergeladen werden. Das Puzzle wird jedoch lustiger (und oft schwieriger), wenn es auf einem Bild aus Ihrer eigenen Fotobibliothek basiert.
Diese Version des 14-15 Puzzles ist Teil der Beispielanwendung und besteht aus einer Reihe von Seiten mit dem Titel Photo Puzzle.
Die Datei "PhotoPuzzlePage1.xaml " besteht aus einem 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 Konstuctor 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 besonders ein Problem mit iOS-Geräten.) 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 (im Uhrzeigersinn), 90° Links (gegen den Uhrzeigersinn) und Fertig.
Die CodeBehind-Datei implementiert die Bitmapdrehungslogik, die im Artikel Erstellen und Zeichnen auf SkiaSharp Bitmaps gezeigt wird. Der Benutzer kann das Bild um 90 Grad im Uhrzeigersinn oder im Uhrzeigersinn beliebig oft 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
der letzten gedrehten Bitmap im Konstruktor der Seite.
PhotoPuzzlePage3
ermöglicht das Zuschneiden des Fotos. Das Programm erfordert eine quadratische Bitmap, um in ein 4:4-Raster von Kacheln aufzuteilen.
Die Datei "PhotoPuzzlePage3.xaml " enthält eine Label
, eine Grid
zum Hosten der PhotoCropperCanvasView
Schaltfläche 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
Bitmap, die an den Konstruktor übergeben wurde. Beachten Sie, dass ein 1 als zweites Argument übergeben wird.PhotoCropperCanvasView
Dieses Seitenverhältnis von 1 erzwingt das Zuschneiderechteck als Quadrat:
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 auf, von denen jede 1/4 die Breite und Höhe des Originals ist. (Die letzte der möglichen 16 Bitmaps wird nicht erstellt.) Mit der DrawBitmap
Methode mit Quell- und Zielrechteck kann eine Bitmap basierend auf einer Teilmenge einer größeren Bitmap erstellt werden.
Konvertieren in Xamarin.Forms Bitmaps
In der OnDoneButtonClicked
Methode ist das für die 15 Bitmaps erstellte Array vom Typ ImageSource
:
ImageSource[] imgSources = new ImageSource[15];
ImageSource
ist der Basistyp, der Xamarin.Forms eine Bitmap kapselt. Glücklicherweise ermöglicht SkiaSharp das Konvertieren von SkiaSharp-Bitmaps in Xamarin.Forms Bitmaps. Die SkiaSharp.Views.Forms-Assembly definiert eine SKBitmapImageSource
Klasse, die von ImageSource
der 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 PhotoPuzzlePage4
an übergeben. Diese Seite ist vollständig Xamarin.Forms und verwendet keinen SkiaSharp. Es ist sehr ähnlich wie XamagonXuzzle, daher wird es hier nicht beschrieben, aber es zeigt Ihr ausgewähltes Foto, das in 15 quadratische Kacheln unterteilt ist:
Durch Drücken der Schaltfläche "Randomize " werden alle Kacheln vermischt:
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.