创建和绘制 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 个正方形图块来显示:
按“随机化”按钮会混合所有图块:
现在可以将其按正确的顺序放回。 可以点击与空白方块位于同一行或同一列的任何图块,以将其移动到空白方块中。



