裁剪 SkiaSharp 位图

创建和绘制 SkiaSharp 位图一文介绍了如何将 SKBitmap 对象传递给 SKCanvas 构造函数。 在该画布上调用的任何绘图方法都会导致在位图上渲染图形。 这些绘图方法包括 DrawBitmap,这意味着该技术允许将一个位图的一部分或全部传输到另一位图(可能会应用变换)。

可以使用该技术通过结合源矩形和目标矩形调用 DrawBitmap 方法来裁剪位图:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

但是,实现裁剪的应用程序通常会提供一个接口,供用户以交互方式选择裁剪矩形:

裁剪示例

本文重点介绍该接口。

封装裁剪矩形

将一些裁剪逻辑隔离在名为 CroppingRectangle 的类中会很有帮助。 构造函数参数包括最大矩形(通常是被裁剪的位图的大小)和可选的纵横比。 构造函数首先定义一个初始裁剪矩形,并在类型为 SKRectRect 属性中将其公开。 此初始裁剪矩形是位图矩形的宽度和高度的 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 类由派生自 SKCanvasViewPhotoCropperCanvasView 类使用。 此类负责显示位图和裁剪矩形,以及处理触摸或鼠标事件以更改裁剪矩形。

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 处理程序调用 CroppingRectangleHitTest 方法。 如果返回的索引不是 –1,则表示正在操作裁剪矩形的一个角。 该索引和实际触摸点距角的偏移量存储在 TouchPoint 对象中,并添加到 touchPoints 字典中。

对于 Moved 事件,将调用 CroppingRectangleMoveCorner 方法来移动角点,并在可能的情况下调整纵横比。

使用 PhotoCropperCanvasView 的程序可以随时访问 CroppedBitmap 属性。 此属性使用 CroppingRectangleRect 属性来创建裁剪大小的新位图。 然后,带有目标和源矩形的 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);
    }
}

然后用户可以操控裁剪矩形:

照片裁剪器 1

定义适当的裁剪矩形后,单击“完成”按钮Clicked 处理程序从 PhotoCropperCanvasViewCroppedBitmap 属性获取裁剪的位图,并将页的所有内容替换为显示此裁剪位图的新 SKCanvasView 对象:

照片裁剪器 2

尝试将 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、一个用于承载 PhotoCropperCanvasViewGrid,以及另一个“完成”按钮

<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 程序集定义一个派生自 ImageSourceSKBitmapImageSource 类,但该类可以基于 SkiaSharp SKBitmap 对象创建。 SKBitmapImageSource 甚至定义了 SKBitmapImageSourceSKBitmap 之间的转换,这就是 SKBitmap 对象作为 Xamarin.Forms 位图存储在数组中的方式:

imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;

此位图数组作为构造函数传递给 PhotoPuzzlePage4。 该页完全是 Xamarin.Forms 并且不使用任何 SkiaSharp。 它与 XamagonXuzzle 非常相似,因此此处不再对其进行介绍,但它会将选择的照片分为 15 个正方形图块来显示:

照片拼图 1

按“随机化”按钮会混合所有图块

照片拼图 2

现在可以将其按正确的顺序放回。 可以点击与空白方块位于同一行或同一列的任何图块,以将其移动到空白方块中。