Обрезка растровых карт SkiaSharp
В статье "Создание и рисование bitmaps skiaSharp" описано, как SKBitmap
объект может быть передан конструкторуSKCanvas
. Любой метод рисования, вызываемый на этом холсте, приводит к отображению графики на растровом рисунке. Эти методы рисования включают в себя DrawBitmap
, что означает, что этот метод позволяет передавать часть или все одно растровое изображение на другое растровое изображение, возможно, с примененными преобразованиями.
Этот метод можно использовать для обрезки растрового изображения, вызвав DrawBitmap
метод с помощью прямоугольников источника и назначения:
canvas.DrawBitmap(bitmap, sourceRect, destRect);
Однако приложения, реализующие обрезку, часто предоставляют интерфейс для пользователя для интерактивного выбора прямоугольника обрезки:
В этой статье рассматривается этот интерфейс.
Инкапсулирование прямоугольника обрезки
Полезно изолировать некоторые логики обрезки в классе с именем CroppingRectangle
. Параметры конструктора включают максимальный прямоугольник, который обычно является размером обрезаемого растрового изображения и необязательным пропорциям. Конструктор сначала определяет начальный прямоугольник обрезки, который делает его общедоступным в свойстве Rect
типа SKRect
. Этот начальный прямоугольник обрезки составляет 80 % ширины и высоты прямоугольника растрового изображения, но затем корректируется, если задано соотношение аспектов:
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; }
···
}
Одна полезная часть информации, которая CroppingRectangle
также делает доступной— это массив SKPoint
значений, соответствующих четырем углам прямоугольника обрезки в порядке верхнего-левого, правого верхнего, нижнего и нижнего левого:
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)
};
}
}
···
}
Этот массив используется в следующем методе, который вызывается HitTest
. Параметр SKPoint
— это точка, соответствующая сенсорному щелчку пальца или щелчку мыши. Метод возвращает индекс (0, 1, 2 или 3), соответствующий углу, который касался пальца или указателя мыши, в пределах расстояния, заданного radius
параметром:
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;
}
···
}
Если точка касания или мыши не находилась в radius
нескольких единицах угла, метод возвращает значение –1.
Последний метод CroppingRectangle
вызывается MoveCorner
, который вызывается в ответ на касание или перемещение мыши. Два параметра указывают индекс перемещаемого угла и новое расположение этого угла. Первая половина метода корректирует прямоугольник обрезки на основе нового расположения угла, но всегда в пределах границ maxRect
, который является размером растрового изображения. Эта логика также учитывает MINIMUM
поле, чтобы избежать сворачивания прямоугольника обрезки в ничего:
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;
}
}
Вторая половина метода корректируется для необязательного пропорции.
Помните, что все в этом классе находится в единицах пикселей.
Представление холста только для обрезки
Класс CroppingRectangle
, который вы только что видели, используется классом PhotoCropperCanvasView
, производным от SKCanvasView
. Этот класс отвечает за отображение растрового изображения и прямоугольника обрезки, а также обработку событий касания или мыши для изменения прямоугольника обрезки.
Конструктору PhotoCropperCanvasView
требуется растровое изображение. Пропорции являются необязательными. Конструктор создает экземпляр объекта типа CroppingRectangle
на основе этого растрового изображения и пропорции и сохраняет его в виде поля:
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);
···
}
···
}
Так как этот класс является производным от SKCanvasView
, не требуется устанавливать обработчик для PaintSurface
события. Вместо этого он может переопределить его OnPaintSurface
метод. Метод отображает растровое изображение и использует несколько объектов, сохраненных в качестве полей для рисования текущего SKPaint
прямоугольника обрезки:
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);
}
···
}
Код в CroppingRectangle
классе основывает прямоугольник обрезки на размер пикселя растрового изображения. Однако отображение растрового изображения PhotoCropperCanvasView
классом масштабируется на основе размера области отображения. Вычисляемый bitmapScaleMatrix
в OnPaintSurface
переопределении сопоставляется с пикселями растрового изображения на размер и позицию растрового изображения, как оно отображается. Затем эта матрица используется для преобразования прямоугольника обрезки, чтобы его можно было отобразить относительно растрового изображения.
Последняя строка OnPaintSurface
переопределения принимает обратное значение bitmapScaleMatrix
и сохраняет его в качестве inverseBitmapMatrix
поля. Это используется для обработки сенсорной обработки.
TouchEffect
Объект создается в виде поля, и конструктор присоединяет обработчик к TouchAction
событию, но TouchEffect
его необходимо добавить Effects
в коллекцию родительского элемента производногоSKCanvasView
, чтобы выполнить OnParentSet
переопределение:
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));
}
}
События касания, обработанные обработчиком TouchAction
, находятся в независимых от устройства единицах. Сначала их необходимо преобразовать в пиксели с помощью ConvertToPixel
метода в нижней части класса, а затем преобразовать в CroppingRectangle
единицы с помощью inverseBitmapMatrix
.
Для Pressed
событий TouchAction
обработчик вызывает HitTest
метод CroppingRectangle
. Если это возвращает индекс, отличный от –1, то один из углов прямоугольника обрезки обрабатывается. Этот индекс и смещение фактической точки касания с угла хранятся в объекте и добавляются touchPoints
в TouchPoint
словарь.
Moved
Для события MoveCorner
вызывается метод CroppingRectangle
перемещения угла с возможными корректировками пропорции.
В любое время программа, использующий PhotoCropperCanvasView
его, может получить доступ к свойству CroppedBitmap
. Это свойство использует Rect
свойство CroppingRectangle
для создания растрового изображения обрезанного размера. Затем версия DrawBitmap
с целевыми и исходными прямоугольниками извлекает подмножество исходного растрового изображения:
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;
}
}
···
}
Размещение представления холста обрезки фотографий
В этих двух классах, обрабатывающих логику обрезки, страница "Обрезка фотографий" в примере приложения очень мало работы. XAML-файл создает экземпляр узла Grid
PhotoCropperCanvasView
и кнопку "Готово ":
<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>
Невозможно PhotoCropperCanvasView
создать экземпляр в XAML-файле, так как для него требуется параметр типа SKBitmap
.
Вместо этого PhotoCropperCanvasView
экземпляр создается в конструкторе файла программной части с помощью одной из растровых карт ресурсов:
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);
}
}
Затем пользователь может управлять прямоугольником обрезки:
Когда был определен хороший прямоугольник обрезки, нажмите кнопку "Готово ". Обработчик Clicked
получает обрезанное растровое изображение из CroppedBitmap
свойства PhotoCropperCanvasView
и заменяет все содержимое страницы новым SKCanvasView
объектом, отображающим это обрезанное растровое изображение:
Попробуйте задать второй аргумент PhotoCropperCanvasView
1,78f (например:
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
Вы увидите прямоугольник обрезки, ограниченный 16-к-9 пропорции, характерные для высокоопределенного телевизора.
Разделение растрового изображения на плитки
Версия Xamarin.Forms знаменитой головоломки 14-15 появилась в главе 22 книги Создание мобильных приложений сXamarin.Formsи может быть скачан как XamagonXuzzle. Однако головоломка становится более веселой (и часто более сложной), когда она основана на изображении из собственной фототеки.
Эта версия головоломки 14-15 является частью примера приложения, и состоит из серии страниц под названием Photo Puzzle.
Файл PhotoPuzzlePage1.xaml состоит из 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>
Файл программной части реализует Clicked
обработчик, использующий службу зависимостей, чтобы IPhotoLibrary
пользователь выбрал фотографию из библиотеки фотографий:
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));
}
}
}
}
Затем метод переходит PhotoPuzzlePage2
к , передавая констектор выбранному растровом рисунку.
Возможно, фотография, выбранная из библиотеки, не ориентирована, как она появилась в фототеке, но вращается или перевернута. (Это особенно проблема с устройствами iOS.) По этой причине PhotoPuzzlePage2
можно повернуть изображение в нужную ориентацию. XAML-файл содержит три кнопки, помеченные как 90° вправо (то есть по часовой стрелке), 90° слева (счетчик по часовой стрелке) и "Готово".
Файл программной части реализует логику поворота растрового изображения, показанную в статье "Создание и рисование на растровых картах SkiaSharp". Пользователь может повернуть изображение 90 градусов по часовой стрелке или по часовой стрелке в любое количество раз:
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));
}
}
Когда пользователь нажимает PhotoPuzzlePage3
кнопку "Готово", Clicked
обработчик переходит к окончательному повернутому растровому рисунку в конструкторе страницы.
PhotoPuzzlePage3
позволяет обрезать фотографию. Программа требует, чтобы квадратная растровая карта разделяла на сетку 4-4 плиток.
Файл PhotoPuzzlePage3.xaml содержит файл Label
, Grid
для размещения PhotoCropperCanvasView
и другой кнопки "Готово":
<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>
Файл программной части создает PhotoCropperCanvasView
экземпляр растрового изображения, переданного конструктору. Обратите внимание, что 1 передается в качестве второго аргумента PhotoCropperCanvasView
. Это соотношение пропорций от 1 заставляет прямоугольник обрезки быть квадратом:
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));
}
}
Обработчик кнопки "Готово " получает ширину и высоту обрезанного растрового изображения (эти два значения должны совпадать), а затем делит его на 15 отдельных растровых изображений, каждая из которых составляет 1/4 ширины и высоты исходного. (Последний из возможных 16 растровых карт не создается.) Метод DrawBitmap
с прямоугольником источника и назначения позволяет создавать растровое изображение на основе подмножества более крупного растрового изображения.
Преобразование в Xamarin.Forms растровые изображения
В методе OnDoneButtonClicked
массив, созданный для 15 растровых изображений, имеет тип ImageSource
:
ImageSource[] imgSources = new ImageSource[15];
ImageSource
— базовый Xamarin.Forms тип, инкапсулирующий растровое изображение. К счастью, SkiaSharp позволяет преобразовать из растровых карт SkiaSharp в Xamarin.Forms растровые изображения. Сборка SkiaSharp.Views.Forms определяет класс, производный SKBitmapImageSource
от ImageSource
объекта SkiaSharp SKBitmap
. SKBitmapImageSource
Даже определяет преобразования между SKBitmapImageSource
и SKBitmap
, а также то, как SKBitmap
объекты хранятся в массиве в виде Xamarin.Forms растровых изображений:
imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
Этот массив растровых изображений передается в качестве конструктора PhotoPuzzlePage4
. Эта страница полностью Xamarin.Forms и не использует skiaSharp. Он очень похож на XamagonXuzzle, поэтому он не будет описан здесь, но отображает выбранную фотографию, разделенную на 15 квадратных плиток:
Нажатие кнопки Randomize смешает все плитки:
Теперь их можно вернуть в правильном порядке. Любые плитки в той же строке или столбце, что и пустой квадрат, можно коснуться, чтобы переместить их в пустой квадрат.