Przycinanie map bitowych SkiaSharp
W artykule Creating and Drawing SkiaSharp Bitmaps (Tworzenie i rysowanie map bitowych SkiaSharp) opisano sposób SKBitmap
przekazywania obiektu do konstruktora SKCanvas
. Każda metoda rysunku wywoływana na tej kanwie powoduje renderowanie grafiki na mapie bitowej. Metody rysowania obejmują DrawBitmap
metodę , co oznacza, że ta technika umożliwia przesyłanie części lub wszystkich jednej mapy bitowej do innej mapy bitowej, być może z zastosowanymi transformacjami.
Możesz użyć tej techniki do przycinania mapy bitowej, wywołując metodę DrawBitmap
z prostokątami źródłowymi i docelowymi:
canvas.DrawBitmap(bitmap, sourceRect, destRect);
Jednak aplikacje implementujące przycinanie często udostępniają użytkownikowi interfejs do interaktywnego wybierania prostokąta przycinania:
Ten artykuł koncentruje się na tym interfejsie.
Hermetyzowanie prostokąta przycinania
Warto odizolować część logiki przycinania w klasie o nazwie CroppingRectangle
. Parametry konstruktora obejmują maksymalny prostokąt, który jest zazwyczaj rozmiarem przycinanej mapy bitowej i opcjonalnym współczynnikiem proporcji. Konstruktor najpierw definiuje początkowy prostokąt przycinania, który upublicznia we Rect
właściwości typu SKRect
. Ten początkowy prostokąt przycinania wynosi 80% szerokości i wysokości prostokąta mapy bitowej, ale jest dostosowywany, jeśli określono współczynnik proporcji:
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; }
···
}
Jedną z przydatnych informacji, które CroppingRectangle
również udostępniają, jest tablica SKPoint
wartości odpowiadających czterem rogom prostokąta przycinania w kolejności w lewym górnym rogu, prawym górnym, prawym dolnym i lewym dolnym:
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)
};
}
}
···
}
Ta tablica jest używana w następującej metodzie, która jest nazywana HitTest
. Parametr SKPoint
jest punktem odpowiadającym dotykowi palca lub kliknięciu myszą. Metoda zwraca indeks (0, 1, 2 lub 3) odpowiadający narożnikowi, którego dotknął palec lub wskaźnik myszy w odległości podanej radius
przez parametr :
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;
}
···
}
Jeśli dotyk lub wskaźnik myszy nie mieścił się w żadnym radius
rogu, metoda zwraca wartość –1.
Ostateczna metoda w metodzie CroppingRectangle
nosi nazwę MoveCorner
, która jest wywoływana w odpowiedzi na ruch dotyku lub myszy. Dwa parametry wskazują indeks przenoszonego rogu i nową lokalizację tego rogu. Pierwsza połowa metody dostosowuje prostokąt przycinania na podstawie nowej lokalizacji rogu, ale zawsze w granicach maxRect
elementu , który jest rozmiarem mapy bitowej. Ta logika uwzględnia również pole, MINIMUM
aby uniknąć zwijania prostokąta przycinania w niczym:
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;
}
}
Druga połowa metody dostosowuje się do opcjonalnego współczynnika proporcji.
Pamiętaj, że wszystko w tej klasie jest w jednostkach pikseli.
Widok kanwy tylko do przycinania
Właśnie CroppingRectangle
widziana klasa jest używana przez klasę PhotoCropperCanvasView
, która pochodzi z klasy SKCanvasView
. Ta klasa jest odpowiedzialna za wyświetlanie mapy bitowej i prostokąta przycinania, a także obsługę zdarzeń dotykowych lub myszy w celu zmiany prostokąta przycinania.
Konstruktor PhotoCropperCanvasView
wymaga mapy bitowej. Współczynnik proporcji jest opcjonalny. Konstruktor tworzy wystąpienie obiektu typu CroppingRectangle
na podstawie tej mapy bitowej i współczynnika proporcji i zapisuje go jako pole:
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);
···
}
···
}
Ponieważ ta klasa pochodzi z SKCanvasView
klasy , nie musi instalować programu obsługi dla PaintSurface
zdarzenia. Zamiast tego może zastąpić jego OnPaintSurface
metodę. Metoda wyświetla mapę bitową i używa kilku obiektów zapisanych SKPaint
jako pola, aby narysować bieżący prostokąt przycinania:
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);
}
···
}
Kod w CroppingRectangle
klasie opiera prostokąt przycinania na rozmiarze pikseli mapy bitowej. Jednak wyświetlanie mapy bitowej według PhotoCropperCanvasView
klasy jest skalowane na podstawie rozmiaru obszaru wyświetlania. Obliczona bitmapScaleMatrix
w OnPaintSurface
przesłonięć mapowanie z pikseli mapy bitowej na rozmiar i położenie mapy bitowej w miarę wyświetlania. Następnie ta macierz służy do przekształcania prostokąta przycinania, aby można było go wyświetlić względem mapy bitowej.
Ostatni wiersz OnPaintSurface
przesłonięcia przyjmuje odwrotność bitmapScaleMatrix
obiektu i zapisuje go jako inverseBitmapMatrix
pole. Jest to używane do przetwarzania dotykowego.
TouchEffect
Obiekt jest tworzone jako pole, a konstruktor dołącza program obsługi do TouchAction
zdarzenia, ale TouchEffect
należy go dodać do Effects
kolekcji elementu nadrzędnego pochodnegoSKCanvasView
, aby wykonać OnParentSet
przesłonięcia:
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));
}
}
Zdarzenia dotykowe przetwarzane przez TouchAction
program obsługi znajdują się w jednostkach niezależnych od urządzenia. Najpierw należy je przekonwertować na piksele przy użyciu ConvertToPixel
metody w dolnej części klasy, a następnie przekonwertować na CroppingRectangle
jednostki przy użyciu metody inverseBitmapMatrix
.
W przypadku Pressed
zdarzeń TouchAction
program obsługi wywołuje metodę HitTest
CroppingRectangle
. Jeśli zwraca to indeks inny niż –1, jednym z narożników prostokąta przycinania jest manipulowanie. Ten indeks i przesunięcie rzeczywistego punktu dotykowego z rogu jest przechowywane w TouchPoint
obiekcie i dodawane do słownika touchPoints
.
Moved
W przypadku zdarzenia MoveCorner
metoda CroppingRectangle
metody jest wywoływana w celu przeniesienia rogu z możliwymi korektami współczynnika proporcji.
W dowolnym momencie program używający PhotoCropperCanvasView
programu może uzyskać dostęp do CroppedBitmap
właściwości. Ta właściwość używa Rect
właściwości , CroppingRectangle
aby utworzyć nową mapę bitową przyciętego rozmiaru. Następnie wersja z DrawBitmap
prostokątami docelowymi i źródłowymi wyodrębnia podzbiór oryginalnej mapy bitowej:
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;
}
}
···
}
Hostowanie widoku kanwy przycinania zdjęć
W przypadku tych dwóch klas obsługujących logikę przycinania strona Przycinanie zdjęć w przykładowej aplikacji ma bardzo mało pracy. Plik XAML tworzy wystąpienie elementu w Grid
celu hostowania PhotoCropperCanvasView
przycisku i Gotowe :
<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>
Nie PhotoCropperCanvasView
można utworzyć wystąpienia obiektu w pliku XAML, ponieważ wymaga parametru typu SKBitmap
.
Zamiast tego element PhotoCropperCanvasView
jest tworzone w konstruktorze pliku kodu za pomocą jednej z map bitowych zasobów:
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);
}
}
Użytkownik może następnie manipulować prostokątem przycinania:
Po zdefiniowaniu dobrego przycinania prostokąta kliknij przycisk Gotowe . Procedura Clicked
obsługi uzyskuje przyciętą mapę bitową z CroppedBitmap
właściwości PhotoCropperCanvasView
i zastępuje całą zawartość strony nowym SKCanvasView
obiektem, który wyświetla tę przyciętą mapę bitową:
Spróbuj ustawić drugi argument PhotoCropperCanvasView
1.78f (na przykład):
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
Zobaczysz prostokąt przycinania ograniczony do 16-do-9 współczynnik proporcji telewizora o wysokiej rozdzielczości.
Dzielenie mapy bitowej na kafelki
Wersja Xamarin.Forms słynnej zagadki 14-15 pojawiła się w rozdziale 22 książki Creating Mobile Apps with Xamarin.Formsi można ją pobrać jako XamagonXuzzle. Jednak zagadka staje się bardziej zabawna (i często trudniejsza), gdy jest oparta na obrazie z własnej biblioteki zdjęć.
Ta wersja układanki 14-15 jest częścią przykładowej aplikacji i składa się z serii stron zatytułowanych Photo Puzzle.
Plik PhotoPuzzlePage1.xaml składa się z elementu 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>
Plik związany z kodem implementuje Clicked
program obsługi, który używa IPhotoLibrary
usługi zależności, aby umożliwić użytkownikowi wybranie zdjęcia z biblioteki zdjęć:
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));
}
}
}
}
Następnie metoda przechodzi do PhotoPuzzlePage2
metody , przechodząc do constuctor wybranej mapy bitowej.
Możliwe, że zdjęcie wybrane z biblioteki nie jest zorientowane, jak pokazano w bibliotece zdjęć, ale jest obracane lub do góry nogami. (Jest to szczególnie problem z urządzeniami z systemem iOS). Z tego powodu PhotoPuzzlePage2
można obrócić obraz do żądanej orientacji. Plik XAML zawiera trzy przyciski oznaczone etykietą 90° w prawo (czyli zgodnie z ruchem wskazówek zegara), 90° w lewo (odwrotnie) i Gotowe.
Plik za kodem implementuje logikę obrotu mapy bitowej pokazaną w artykule Tworzenie i rysowanie na mapach bitowych SkiaSharp. Użytkownik może obrócić obraz o 90 stopni zgodnie z ruchem wskazówek zegara lub wskazówek zegara dowolną liczbę razy:
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));
}
}
Gdy użytkownik kliknie przycisk Gotowe , Clicked
program obsługi przechodzi do PhotoPuzzlePage3
elementu , przekazując ostatnią obróconą mapę bitową w konstruktorze strony.
PhotoPuzzlePage3
umożliwia przycięcie zdjęcia. Program wymaga kwadratowej mapy bitowej, aby podzielić się na siatkę 4-by-4 kafelków.
Plik PhotoPuzzlePage3.xaml zawiera Label
element , a Grid
do hostowania PhotoCropperCanvasView
elementu i inny przycisk Gotowe :
<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>
Plik za kodem tworzy wystąpienie PhotoCropperCanvasView
elementu z mapą bitową przekazaną do konstruktora. Zwróć uwagę, że wartość 1 jest przekazywana jako drugi argument do PhotoCropperCanvasView
. Ten współczynnik proporcji 1 wymusza przycinanie prostokąta na kwadrat:
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));
}
}
Procedura obsługi przycisku Gotowe uzyskuje szerokość i wysokość przyciętej mapy bitowej (te dwie wartości powinny być takie same), a następnie dzieli ją na 15 oddzielnych map bitowych, z których każda ma 1/4 szerokość i wysokość oryginału. (Nie utworzono ostatniej z możliwych 16 map bitowych). Metoda DrawBitmap
z prostokątem źródłowym i docelowym umożliwia utworzenie mapy bitowej na podstawie podzbioru większej mapy bitowej.
Konwertowanie na Xamarin.Forms mapy bitowe
W metodzie OnDoneButtonClicked
tablica utworzona dla 15 map bitowych ma typ ImageSource
:
ImageSource[] imgSources = new ImageSource[15];
ImageSource
jest typem Xamarin.Forms podstawowym, który hermetyzuje mapę bitową. Na szczęście SkiaSharp umożliwia konwertowanie map bitowych SkiaSharp na Xamarin.Forms mapy bitowe. Zestaw SkiaSharp.Views.Forms definiuje klasę pochodzącą SKBitmapImageSource
z ImageSource
klasy , ale można ją utworzyć na podstawie obiektu SkiaSharp SKBitmap
. SKBitmapImageSource
Definiuje nawet konwersje między SKBitmapImageSource
i SKBitmap
, a w ten sposób SKBitmap
obiekty są przechowywane w tablicy jako Xamarin.Forms mapy bitowe:
imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
Ta tablica map bitowych jest przekazywana jako konstruktor do PhotoPuzzlePage4
. Ta strona jest całkowicie Xamarin.Forms i nie używa żadnych SkiaSharp. Jest on bardzo podobny do XamagonXuzzle, więc nie zostanie opisany tutaj, ale wyświetla wybrane zdjęcie podzielone na 15 kafelków kwadratowych:
Naciśnięcie przycisku Randomize powoduje wymieszanie wszystkich kafelków:
Teraz możesz umieścić je z powrotem w odpowiedniej kolejności. Wszystkie kafelki w tym samym wierszu lub kolumnie co pusty kwadrat można wykorzystać, aby przenieść je do pustego kwadratu.