SkiaSharp 비트맵 자르기Cropping SkiaSharp bitmaps

샘플 다운로드 샘플 다운로드Download Sample Download the sample

합니다 만들고 그리기 SkiaSharp 비트맵 하는 방법을 설명 하는 문서를 SKBitmap 개체를 전달할 수는 SKCanvas 생성자입니다.The Creating and Drawing SkiaSharp Bitmaps article described how an SKBitmap object can be passed to an SKCanvas constructor. 비트맵에 렌더링 되는 캔버스 원인 그래픽 라는 모든 그리기 메서드.Any drawing method called on that canvas causes graphics to be rendered on the bitmap. 이러한 그리기 메서드를 포함 DrawBitmap, 즉,이 기술은 한 비트맵의 전체 또는 일부 다른 비트맵 아마도 사용 하 여 전송 적용 되는 변환을 허용함.These drawing methods include DrawBitmap, which means that this technique allows transferring part or all of one bitmap to another bitmap, perhaps with transforms applied.

비트맵을 호출 하 여 자르기에 대 한 해당 기법을 사용할 수는 DrawBitmap 소스 및 대상 사각형을 사용 하 여 메서드:You can use that technique for cropping a bitmap by calling the DrawBitmap method with source and destination rectangles:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

그러나 종종 자르기 구현 하는 응용 프로그램을 대화형으로 자르기 사각형을 선택할 사용자에 대 한 인터페이스를 제공 합니다.However, applications that implement cropping often provide an interface for the user to interactively select the cropping rectangle:

샘플 자르기Cropping Sample

이 문서는 해당 인터페이스에 중점을 둡니다.This article focuses on that interface.

자르기 사각형을 캡슐화합니다.Encapsulating the cropping rectangle

라는 클래스에는 자르기 논리 중 일부를 격리 하는 것이 유용 CroppingRectangle합니다.It's helpful to isolate some of the cropping logic in a class named CroppingRectangle. 생성자 매개 변수는 잘리지 비트맵의 크기는 일반적으로 최대 직사각형을 및 선택적 가로 세로 비율이 포함 됩니다.The constructor parameters include a maximum rectangle, which is generally the size of the bitmap being cropped, and an optional aspect ratio. 생성자는 먼저 공개에 있도록 초기 자르기 사각형을 정의 합니다 Rect 형식의 속성 SKRect합니다.The constructor first defines an initial cropping rectangle, which it makes public in the Rect property of type SKRect. 이 초기 자르기 사각형 비트맵 사각형의 높이 및 너비의 80% 이지만 가로 세로 비율을 지정 하는 경우 다음 조정 됩니다.This initial cropping rectangle is 80% of the width and height of the bitmap rectangle, but it is then adjusted if an aspect ratio is specified:

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 왼쪽 위, 오른쪽 위, 아래 오른쪽 및 왼쪽 아래 순서로 자르기 사각형의 네 모퉁이에 해당 하는 값:One useful piece of information that CroppingRectangle also makes available is an array of SKPoint values corresponding to the four corners of the cropping rectangle in the order upper-left, upper-right, lower-right, and lower-left:

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합니다.This array is used in the following method, which is called HitTest. SKPoint 매개 변수는 지점에 해당 하는 손가락 터치 또는 마우스를 클릭 합니다.The SKPoint parameter is a point corresponding to a finger touch or a mouse click. 인덱스 (0, 1, 2 또는 3)를 반환 하 여 지정 된 거리 내 손가락이 나 마우스 포인터를 작업 하는 모퉁이에 해당 합니다 radius 매개 변수:The method returns an index (0, 1, 2, or 3) corresponding to the corner that the finger or mouse pointer touched, within a distance given by the radius parameter:

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입니다.If the touch or mouse point was not within radius units of any corner, the method returns –1.

마지막 메서드로 CroppingRectangle 라고 MoveCorner, 터치 또는 마우스를 이동에 대 한 응답에서 이라고 합니다.The final method in CroppingRectangle is called MoveCorner, which is called in response to touch or mouse movement. 두 매개 변수 인덱스 이동 하는 모퉁이 및 해당 모퉁이의 새 위치를 나타냅니다.The two parameters indicate the index of the corner being moved, and the new location of that corner. 자르기 사각형의 모퉁이 있지만 범위 내에서 항상 새 위치를 기반으로 조정 하는 메서드의 첫 번째 절반 maxRect, 비트맵의 크기는 합니다.The first half of the method adjusts the cropping rectangle based on the new location of the corner, but always within the bounds of maxRect, which is the size of the bitmap. 이 논리도 고려 합니다 MINIMUM nothing 자르기 사각형을 축소 하지 않으려면 필드:This logic also takes account of the MINIMUM field to avoid collapsing the cropping rectangle into nothing:

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

메서드의 두 번째 절반에서는 선택적 가로 세로 비율을 조정합니다.The second half of the method adjusts for the optional aspect ratio.

이 클래스의 모든 픽셀 단위로 점을 염두에 두십시오.Keep in mind that everything in this class is in units of pixels.

자르기에 대 한 캔버스 뷰A canvas view just for cropping

CroppingRectangle 방금 살펴봤습니다 클래스를 사용 합니다 PhotoCropperCanvasView 클래스에서 파생 되는 SKCanvasView합니다.The CroppingRectangle class you've just seen is used by the PhotoCropperCanvasView class, which derives from SKCanvasView. 이 클래스는 자르기 사각형 변경에 대 한 터치 또는 마우스 이벤트를 처리할 수 있을 뿐만 아니라 비트맵 및 자르기 사각형을 표시 하는 일을 담당 합니다.This class is responsible for displaying the bitmap and the cropping rectangle, as well as handling touch or mouse events for changing the cropping rectangle.

PhotoCropperCanvasView 생성자 비트맵에 필요 합니다.The PhotoCropperCanvasView constructor requires a bitmap. 가로 세로 비율은 선택 사항입니다.An aspect ratio is optional. 생성자는 형식의 개체를 인스턴스화합니다 CroppingRectangle 이 비트맵 및 가로 세로 비율을 기반으로 하며 필드로 저장 합니다.The constructor instantiates an object of type CroppingRectangle based on this bitmap and aspect ratio and saves it as a field:

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 이벤트입니다.Because this class derives from SKCanvasView, it doesn't need to install a handler for the PaintSurface event. 대신을 재정의할 수 있습니다 해당 OnPaintSurface 메서드.It can instead override its OnPaintSurface method. 메서드는 비트맵을 표시 하 고 몇 가지를 사용 하 여 SKPaint 현재 자르기 사각형을 그릴 필드로 저장 된 개체:The method displays the bitmap and uses a couple of SKPaint objects saved as fields to draw the current cropping rectangle:

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 클래스 자르기 사각형 비트맵의 픽셀 크기를 기반 합니다.The code in the CroppingRectangle class bases the cropping rectangle on the pixel size of the bitmap. 그러나 하 여 비트맵의 표시를 PhotoCropperCanvasView 클래스 표시 영역의 크기에 따라 확장 됩니다.However, the display of the bitmap by the PhotoCropperCanvasView class is scaled based on the size of the display area. bitmapScaleMatrix 에서 계산 된 OnPaintSurface 표시 되는 비트맵의 위치 및 크기가 비트맵 픽셀의 지도 재정의 합니다.The bitmapScaleMatrix calculated in the OnPaintSurface override maps from the bitmap pixels to the size and position of the bitmap as it is displayed. 이 매트릭스 비트맵을 기준으로 표시 될 수 있도록 자르기 사각형을 변환에 사용 됩니다.This matrix is then used to transform the cropping rectangle so that it can be displayed relative to the bitmap.

마지막 줄을 OnPaintSurface 재정의의 역함수 값을 사용 합니다 bitmapScaleMatrix 로 저장 하 고는 inverseBitmapMatrix 필드.The last line of the OnPaintSurface override takes the inverse of the bitmapScaleMatrix and saves it as the inverseBitmapMatrix field. 터치 처리를 위해 사용 됩니다.This is used for touch processing.

TouchEffect 필드로 개체가 인스턴스화되고 생성자에 처리기를 연결 합니다 TouchAction 이벤트 하지만 TouchEffect 에 추가 해야는 Effects 의 컬렉션을 부모 합니다 의SKCanvasView완료 되도록 파생 된 OnParentSet 재정의:A TouchEffect object is instantiated as a field, and the constructor attaches a handler to the TouchAction event, but the TouchEffect needs to be added to the Effects collection of the parent of the SKCanvasView derivative, so that's done in the OnParentSet override:

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 처리기는 장치 독립적 단위입니다.The touch events processed by the TouchAction handler are in device-independent units. 먼저 사용 하 여 픽셀 변환할 필요가 합니다 ConvertToPixel 클래스의 맨 아래에 있는 메서드를 변환한 후 CroppingRectangle 사용 하 여 단위 inverseBitmapMatrix.These first need to be converted to pixels using the ConvertToPixel method at the bottom of the class, and then converted to CroppingRectangle units using inverseBitmapMatrix.

에 대 한 Pressed 이벤트를 TouchAction 처리기 호출을 HitTest 메서드의 CroppingRectangle합니다.For Pressed events, the TouchAction handler calls the HitTest method of CroppingRectangle. 이외의 인덱스를 반환 하는 경우 –1 다음 자르기 사각형의 모서리 중 하나를 조작 중인 합니다.If this returns an index other than –1, then one of the corners of the cropping rectangle is being manipulated. 인덱스 및 모서리에서 실제 터치 지점의 오프셋에 저장 되는 TouchPoint 개체를 추가할는 touchPoints 사전입니다.That index and an offset of the actual touch point from the corner is stored in a TouchPoint object and added to the touchPoints dictionary.

에 대 한 합니다 Moved 이벤트를 MoveCorner 메서드의 CroppingRectangle 가로 세로 비율에 대 한 가능한 조정 모퉁이 이동 하기 위해 호출 됩니다.For the Moved event, the MoveCorner method of CroppingRectangle is called to move the corner, with possible adjustments for the aspect ratio.

언제 든 지 사용 하 여 프로그램이 PhotoCropperCanvasView 액세스할 수는 CroppedBitmap 속성입니다.At any time, a program using PhotoCropperCanvasView can access the CroppedBitmap property. 이 속성에 사용 되는 Rect 의 속성을 CroppingRectangle 자른된 크기의 새 비트맵을 만들 수입니다.This property uses the Rect property of the CroppingRectangle to create a new bitmap of the cropped size. 버전 DrawBitmap 대상 및 소스를 사용 하 여 사각형 다음 추출 원래 비트맵의 하위 집합:The version of DrawBitmap with destination and source rectangles then extracts a subset of the original bitmap:

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

사진 cropper 캔버스 뷰를 호스팅Hosting the photo cropper canvas view

자르기 논리를 처리 하는 이러한 두 클래스를 사용 하 여는 사진 자르기 페이지에 SkiaSharpFormsDemos 응용 프로그램에 거의 작업을 수행 합니다.With those two classes handling the cropping logic, the Photo Cropping page in the SkiaSharpFormsDemos application has very little work to do. XAML 파일은는 Grid 호스트에는 PhotoCropperCanvasView수행 단추:The XAML file instantiates a Grid to host the PhotoCropperCanvasView and a Done button:

<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합니다.The PhotoCropperCanvasView cannot be instantiated in the XAML file because it requires a parameter of type SKBitmap.

대신는 PhotoCropperCanvasView 리소스 비트맵 중 하나를 사용 하 여 코드 숨김 파일의 생성자에서 인스턴스화됩니다.Instead, the PhotoCropperCanvasView is instantiated in the constructor of the code-behind file using one of the resource bitmaps:

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

그런 다음 자르기 사각형을 조작할 수 있습니다.:The user can then manipulate the cropping rectangle:

Cropper 1 사진Photo Cropper 1

정의한 좋은 자르기 사각형을 클릭 합니다 수행 단추입니다.When a good cropping rectangle has been defined, click the Done button. Clicked 처리기에서 자른된 비트맵을 가져옵니다 합니다 CroppedBitmap 속성을 PhotoCropperCanvasView를 새 페이지의 모든 콘텐츠를 대체 하 고 SKCanvasView 이 자른된 비트맵을 표시 하는 개체:The Clicked handler obtains the cropped bitmap from the CroppedBitmap property of PhotoCropperCanvasView, and replaces all the content of the page with a new SKCanvasView object that displays this cropped bitmap:

Cropper 2 사진Photo Cropper 2

두 번째 인수를 설정 해 보려면 PhotoCropperCanvasView 1.78f (예:)을 합니다.Try setting the second argument of PhotoCropperCanvasView to 1.78f (for example):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

높음-텔레비전의 특성에 16-9 가로 세로 비율 제한 자르기 사각형을 볼 수 있습니다.You'll see the cropping rectangle restricted to a 16-to-9 aspect ratio characteristic of high-definition television.

비트맵을 타일로 분Dividing a bitmap into tiles

책의 22 장에에서 표시 되는 유명한 Xamarin.Forms 버전 14 ~ 15 퍼즐 Creating Mobile Apps with Xamarin.Forms 로 다운로드할 수 있습니다 XamagonXuzzle합니다.A Xamarin.Forms version of the famous 14-15 puzzle appeared in Chapter 22 of the book Creating Mobile Apps with Xamarin.Forms and can be downloaded as XamagonXuzzle. 그러나 퍼즐 됩니다 더 재미 있게 (및 더 까다로운 종종) 자신의 사진 라이브러리에서 이미지에 기반 하는 경우.However, the puzzle becomes more fun (and often more challenging) when it is based on an image from your own photo library.

14 ~ 15 퍼즐의이 버전의 일부인 합니다 SkiaSharpFormsDemos 응용 프로그램에는 일련의 페이지가 이라는 구성 됩니다 사진 퍼즐.This version of the 14-15 puzzle is part of the SkiaSharpFormsDemos application, and consists of a series of pages titled Photo Puzzle.

합니다 PhotoPuzzlePage1.xaml 구성 파일을 Button:The PhotoPuzzlePage1.xaml file consists of a 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 종속성 서비스 사용자가 사진 라이브러리에서 사진을 선택:The code-behind file implements a Clicked handler that uses the IPhotoLibrary dependency service to let the user pick a photo from the photo library:

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선택한 비트맵 생성자에 전달 합니다.The method then navigates to PhotoPuzzlePage2, passing to the constuctor the selected bitmap.

사진 라이브러리에 표시 되 고 있지만 회전 또는 거꾸로 라이브러리에서 선택한 사진 지향 아님을 가능성이 있습니다.It's possible that the photo selected from the library is not oriented as it appeared in the photo library, but is rotated or upside-down. (IOS 장치를 나열 하는 특히 문제가 됩니다.) 이런 이유로 PhotoPuzzlePage2 원하는 방향으로 이미지를 회전할 수 있습니다.(This is particularly a problem with iOS devices.) For that reason, PhotoPuzzlePage2 allows you to rotate the image to a desired orientation. XAML 파일에 레이블이 지정 된 세 가지 단추가 90° 오른쪽 (즉 시계 방향으로), 90° 왼쪽 (시계 반대 방향으로), 및 완료.The XAML file contains three buttons labeled 90° Right (meaning clockwise), 90° Left (counterclockwise), and Done.

이 문서에 표시 된 비트맵 회전 논리를 구현 하는 코드 숨김 파일을 만들고 SkiaSharp 비트맵에 드로잉 합니다.The code-behind file implements the bitmap-rotation logic shown in the article Creating and Drawing on SkiaSharp Bitmaps. 사용자 이미지를 시계 방향 또는 시계 반대 방향으로 90도 원하는 횟수 만큼 회전 수 있습니다.:The user can rotate the image 90 degrees clockwise or counter-clockwise any number of times:

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, 최종 회전된 비트맵 페이지의 생성자에 전달 합니다.When the user clicks the Done button, the Clicked handler navigates to PhotoPuzzlePage3, passing the final rotated bitmap in the page's constructor.

PhotoPuzzlePage3 잘라야 사진을 허용 합니다.PhotoPuzzlePage3 allows the photo to be cropped. 프로그램-4x4 표 형태 타일을 나누는 데 사각형 비트맵에 필요 합니다.The program requires a square bitmap to divide into a 4-by-4 grid of tiles.

PhotoPuzzlePage3.xaml 파일에는 Label, Grid 호스트에는 PhotoCropperCanvasView, 또 다른 수행 단추:The PhotoPuzzlePage3.xaml file contains a Label, a Grid to host the PhotoCropperCanvasView, and another Done button:

<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 해당 생성자에 전달 된 비트맵입니다.The code-behind file instantiates the PhotoCropperCanvasView with the bitmap passed to its constructor. 1은 두 번째 인수로 전달 되는 알림 PhotoCropperCanvasView합니다.Notice that a 1 is passed as the second argument to PhotoCropperCanvasView. 1의 가로 세로 비율이이 강제로 정사각형이 자르기 사각형:This aspect ratio of 1 forces the cropping rectangle to be a square:

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

합니다 수행 단추 처리기 (이 두 값 같아야) 자른된 비트맵의 높이 너비를 가져오고 다음 1/4는 각각 별도 비트맵 15 나눕니다 원래 높이 너비입니다.The Done button handler obtains the width and height of the cropped bitmap (these two values should be the same) and then divides it into 15 separate bitmaps, each of which is 1/4 the width and height of the original. (가능한 16 비트맵의 마지막 생성 되지 않습니다.) DrawBitmap 원본 및 대상 사각형을 사용 하 여 메서드 비트맵을 더 큰 비트맵의 하위 집합에 따라 만들 수 있습니다.(The last of the possible 16 bitmaps is not created.) The DrawBitmap method with source and destination rectangle allows a bitmap to be created based on subset of a larger bitmap.

Xamarin.Forms 비트맵으로 변환Converting to Xamarin.Forms bitmaps

OnDoneButtonClicked 메서드를 15 비트맵 만든 배열 유형임 ImageSource :In the OnDoneButtonClicked method, the array created for the 15 bitmaps is of type ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource 비트맵을 캡슐화 하는 Xamarin.Forms 기본 형식이입니다.ImageSource is the Xamarin.Forms base type that encapsulates a bitmap. 다행 스럽게도 SkiaSharp 비트맵 Xamarin.Forms에서 SkiaSharp 비트맵 변환할 수 있습니다.Fortunately, SkiaSharp allows converting from SkiaSharp bitmaps to Xamarin.Forms bitmaps. SkiaSharp.Views.Forms 어셈블리 정의 SKBitmapImageSource 에서 파생 된 클래스 ImageSource SkiaSharp을 따라 만들 수 있습니다 하지만 SKBitmap 개체입니다.The SkiaSharp.Views.Forms assembly defines an SKBitmapImageSource class that derives from ImageSource but can be created based on a SkiaSharp SKBitmap object. SKBitmapImageSource 간의 변환도 정의 SKBitmapImageSourceSKBitmap, 및는 어떻게 SKBitmap 개체 Xamarin.Forms 비트맵으로 배열에 저장 됩니다:SKBitmapImageSource even defines conversions between SKBitmapImageSource and SKBitmap, and that's how SKBitmap objects are stored in an array as Xamarin.Forms bitmaps:

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

이 배열을 비트맵을 생성자로 전달 됩니다 PhotoPuzzlePage4합니다.This array of bitmaps is passed as a constructor to PhotoPuzzlePage4. 해당 페이지는 Xamarin.Forms 완전히 및 모든 SkiaSharp 사용 하지 않습니다.That page is entirely Xamarin.Forms and doesn't use any SkiaSharp. 매우 비슷합니다 XamagonXuzzle이므로 여기서 설명 하지는 않지만 15 정사각형 타일로 구분 하 여 선택한 사진 표시:It is very similar to XamagonXuzzle, so it won't be described here, but it displays your selected photo divided into 15 square tiles:

퍼즐 1 사진Photo Puzzle 1

키를 눌러 합니다 임의 모든 타일을 혼합 하는 단추:Pressing the Randomize button mixes up all the tiles:

퍼즐 2 사진Photo Puzzle 2

이제 올바른 순서로 이러한을 넣을 수 있습니다.Now you can put them back in the correct order. 빈 사각형 폴더로 이동 하 동일한 행 또는 열을 빈 사각형의 모든 타일을 탭 할 수 있습니다.Any tiles in the same row or column as the blank square can be tapped to move them into the blank square.