Обрезка растровых карт 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-файл создает экземпляр узла GridPhotoCropperCanvasView и кнопку "Готово ":

<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);
    }
}

Затем пользователь может управлять прямоугольником обрезки:

Фото Обрезка 1

Когда был определен хороший прямоугольник обрезки, нажмите кнопку "Готово ". Обработчик Clicked получает обрезанное растровое изображение из CroppedBitmap свойства PhotoCropperCanvasViewи заменяет все содержимое страницы новым SKCanvasView объектом, отображающим это обрезанное растровое изображение:

Фото Обрезка 2

Попробуйте задать второй аргумент 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 квадратных плиток:

Фото головоломка 1

Нажатие кнопки Randomize смешает все плитки:

Фото Головоломка 2

Теперь их можно вернуть в правильном порядке. Любые плитки в той же строке или столбце, что и пустой квадрат, можно коснуться, чтобы переместить их в пустой квадрат.