Обрезка растровых карт 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 смешает все плитки:
Теперь их можно вернуть в правильном порядке. Любые плитки в той же строке или столбце, что и пустой квадрат, можно коснуться, чтобы переместить их в пустой квадрат.