裁剪 SkiaSharp 位图
创建和绘制 SkiaSharp 位图一文介绍了如何将 SKBitmap
对象传递给 SKCanvas
构造函数。 在该画布上调用的任何绘图方法都会导致在位图上渲染图形。 这些绘图方法包括 DrawBitmap
,这意味着该技术允许将一个位图的一部分或全部传输到另一位图(可能会应用变换)。
可以使用该技术通过结合源矩形和目标矩形调用 DrawBitmap
方法来裁剪位图:
canvas.DrawBitmap(bitmap, sourceRect, destRect);
但是,实现裁剪的应用程序通常会提供一个接口,供用户以交互方式选择裁剪矩形:
本文重点介绍该接口。
封装裁剪矩形
将一些裁剪逻辑隔离在名为 CroppingRectangle
的类中会很有帮助。 构造函数参数包括最大矩形(通常是被裁剪的位图的大小)和可选的纵横比。 构造函数首先定义一个初始裁剪矩形,并在类型为 SKRect
的 Rect
属性中将其公开。 此初始裁剪矩形是位图矩形的宽度和高度的 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
类由派生自 SKCanvasView
的 PhotoCropperCanvasView
类使用。 此类负责显示位图和裁剪矩形,以及处理触摸或鼠标事件以更改裁剪矩形。
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
类显示的位图将根据显示区域的大小进行缩放。 在 OnPaintSurface
重写中计算的 bitmapScaleMatrix
从位图像素映射到位图显示时的大小和位置。 然后使用此矩阵来变换裁剪矩形,以便它可以相对于位图显示。
OnPaintSurface
重写的最后一行采用 bitmapScaleMatrix
的倒数并将其保存为 inverseBitmapMatrix
字段。 此字段用于触摸处理。
TouchEffect
对象实例化为字段,构造函数将处理程序附加到 TouchAction
事件,但需要将 TouchEffect
添加到 SKCanvasView
派生项的父级的 Effects
集合,因此该操作是在 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
方法将其转换为像素,然后使用 inverseBitmapMatrix
转换为 CroppingRectangle
单位。
对于 Pressed
事件,TouchAction
处理程序调用 CroppingRectangle
的 HitTest
方法。 如果返回的索引不是 –1,则表示正在操作裁剪矩形的一个角。 该索引和实际触摸点距角的偏移量存储在 TouchPoint
对象中,并添加到 touchPoints
字典中。
对于 Moved
事件,将调用 CroppingRectangle
的 MoveCorner
方法来移动角点,并在可能的情况下调整纵横比。
使用 PhotoCropperCanvasView
的程序可以随时访问 CroppedBitmap
属性。 此属性使用 CroppingRectangle
的 Rect
属性来创建裁剪大小的新位图。 然后,带有目标和源矩形的 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>
无法在 XAML 文件中实例化 PhotoCropperCanvasView
,因为它需要类型为 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
处理程序从 PhotoCropperCanvasView
的 CroppedBitmap
属性获取裁剪的位图,并将页的所有内容替换为显示此裁剪位图的新 SKCanvasView
对象:
尝试将 PhotoCropperCanvasView
的第二个参数设置为 1.78f 之类的值:
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
你将看到,裁剪矩形限制为高清电视的 16:9 纵横比特征。
将位图划分为图块
著名的 14-15 拼图的 Xamarin.Forms 版本出现在使用 Xamarin.Forms 创建移动应用一书的第 22 章中,可以作为 XamagonXuzzle 下载。 但是,当拼图基于你自己的照片图库中的图像时,它会变得更有趣(并且通常更具挑战性)。
此版本的 14-15 拼图是示例应用程序的一部分,由一系列标题为“照片拼图”的页组成。
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));
}
}
当用户单击“完成”按钮时,Clicked
处理程序将导航到 PhotoPuzzlePage3
,并在页的构造函数中传递最终的旋转位图。
PhotoPuzzlePage3
允许裁剪照片。 该程序需要将正方形位图划分为 4×4 的图块网格。
PhotoPuzzlePage3.xaml 文件包含一个 Label
、一个用于承载 PhotoCropperCanvasView
的 Grid
,以及另一个“完成”按钮:
<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 程序集定义一个派生自 ImageSource
的 SKBitmapImageSource
类,但该类可以基于 SkiaSharp SKBitmap
对象创建。 SKBitmapImageSource
甚至定义了 SKBitmapImageSource
和 SKBitmap
之间的转换,这就是 SKBitmap
对象作为 Xamarin.Forms 位图存储在数组中的方式:
imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
此位图数组作为构造函数传递给 PhotoPuzzlePage4
。 该页完全是 Xamarin.Forms 并且不使用任何 SkiaSharp。 它与 XamagonXuzzle 非常相似,因此此处不再对其进行介绍,但它会将选择的照片分为 15 个正方形图块来显示:
按“随机化”按钮会混合所有图块:
现在可以将其按正确的顺序放回。 可以点击与空白方块位于同一行或同一列的任何图块,以将其移动到空白方块中。