다음을 통해 공유


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 손가락 터치 또는 마우스 클릭에 해당하는 지점입니다. 메서드는 매개 변수에서 지정 radius 한 거리 내에서 손가락 또는 마우스 포인터가 닿은 모서리에 해당하는 인덱스(0, 1, 2 또는 3)를 반환합니다.

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 부모 컬렉션에 OnParentSet 추가 Effects 해야 합니다.

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 픽셀로 변환한 다음 사용 단위로 CroppingRectangleinverseBitmapMatrix변환해야 합니다.

이벤트의 경우 Pressed 처리기는 TouchAction .의 메서드를 HitTest 호출합니다 CroppingRectangle. -1 이외의 인덱스를 반환하는 경우 자르기 사각형의 모서리 중 하나가 조작됩니다. 해당 인덱스와 모서리에서 실제 터치 포인트의 오프셋은 개체에 TouchPoint 저장되고 사전에 추가 touchPoints 됩니다.

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 파일은 호스트 PhotoCropperCanvasView및 완료 단추를 인스턴스화합니다Grid.

<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>

형식SKBitmapPhotoCropperCanvasView 매개 변수가 필요하므로 XAML 파일에서 인스턴스화할 수 없습니다.

대신 리소스 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 처리기는 의 속성PhotoCropperCanvasView에서 CroppedBitmap 잘린 비트맵을 가져오고 페이지의 모든 내용을 이 잘린 비트맵을 표시하는 새 SKCanvasView 개체로 바꿉니다.

사진 크롭퍼 2

두 번째 인수 PhotoCropperCanvasView 를 1.78f로 설정해 보세요(예: ).

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

자르기 사각형은 고화질 TV의 16대 9 가로 세로 비율 특성으로 제한됩니다.

비트맵을 타일로 나누기

Xamarin.Forms 유명한 14-15 퍼즐의 버전은 함께 모바일 앱을 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>

코드 숨김 파일은 사용자가 사진 라이브러리에서 사진을 선택할 수 있도록 종속성 서비스를 사용하는 IPhotoLibrary 처리기를 구현 Clicked 합니다.

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선택한 비트맵을 constuctor에 전달합니다.

라이브러리에서 선택한 사진이 사진 라이브러리에 나타난 것처럼 방향이 아니라 회전하거나 거꾸로 되어 있는 것일 수 있습니다. (특히 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 파일에는 호스트PhotoCropperCanvasView할 < Grid a0Label/> 및 다른 완료 단추가 포함되어 있습니다.

<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 어셈블리는 SkiaSharp SKBitmap 개체에서 ImageSource 파생되지만 만들 수 있는 클래스를 정의 SKBitmapImageSource 합니다. SKBitmapImageSource변환을 SKBitmapImageSource 정의하고 SKBitmap개체를 배열에 비트맵으로 Xamarin.Forms 저장하는 방식 SKBitmap 도 정의합니다.

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

이 비트맵 배열은 생성자로 PhotoPuzzlePage4전달됩니다. 해당 페이지는 전적으로 Xamarin.Forms SkiaSharp를 사용하지 않습니다. XamagonXuzzle과 매우 유사하므로 여기에 설명되지는 않지만 선택한 사진을 15 정사각형 타일로 나누어 표시합니다.

사진 퍼즐 1

임의 단추를 누르면 모든 타일이 섞입니다.

사진 퍼즐 2

이제 올바른 순서로 다시 넣을 수 있습니다. 빈 사각형과 동일한 행 또는 열에 있는 모든 타일을 탭하여 빈 사각형으로 이동할 수 있습니다.