Il ritaglio di immagini bitmap in SkiaSharpCropping SkiaSharp bitmaps

Scaricare l'esempio scaricare l'esempioDownload Sample Download the sample

Il creazione e disegno in SkiaSharp bitmap articolo descritta come un SKBitmap oggetto può essere passato a un SKCanvas costruttore.The Creating and Drawing SkiaSharp Bitmaps article described how an SKBitmap object can be passed to an SKCanvas constructor. Qualsiasi metodo di disegno chiamato sull'oggetto grafico cause tale area di disegno deve essere sottoposto a rendering nella bitmap.Any drawing method called on that canvas causes graphics to be rendered on the bitmap. Questi metodi di disegno includono DrawBitmap, il che significa che questa tecnica permette di trasferimento o parte di una singola bitmap a un'altra mappa di bit, probabilmente con trasformazioni applicate.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.

È possibile usare tale tecnica per ritagliare un'immagine bitmap chiamando il DrawBitmap metodo con i rettangoli di origine e di destinazione:You can use that technique for cropping a bitmap by calling the DrawBitmap method with source and destination rectangles:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Tuttavia, le applicazioni che implementano il ritaglio spesso offrono un'interfaccia per l'utente di selezionare in modo interattivo il rettangolo di ritaglio:However, applications that implement cropping often provide an interface for the user to interactively select the cropping rectangle:

Esempio di ritaglioCropping Sample

Questo articolo è incentrato su tale interfaccia.This article focuses on that interface.

Che incapsula il rettangolo di ritaglioEncapsulating the cropping rectangle

È utile isolare parte della logica di ritaglio in una classe denominata CroppingRectangle.It's helpful to isolate some of the cropping logic in a class named CroppingRectangle. I parametri del costruttore includono un rettangolo di massimo, che corrisponde in genere le dimensioni della bitmap viene ritagliata, e le proporzioni facoltativa.The constructor parameters include a maximum rectangle, which is generally the size of the bitmap being cropped, and an optional aspect ratio. Il costruttore prima di tutto definisce un rettangolo di ritaglio iniziale, che rende pubblico nel Rect vlastnosti typu SKRect.The constructor first defines an initial cropping rectangle, which it makes public in the Rect property of type SKRect. Questo rettangolo di ritaglio iniziale è l'80% della larghezza e altezza del rettangolo di bitmap, ma e quindi adeguato se viene specificato un rapporto di aspetto: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; }
    ···
}

Un'informazione utile che CroppingRectangle inoltre rende disponibile è una matrice di SKPoint i valori corrispondenti ai quattro angoli del rettangolo di ritaglio nell'ordine in alto a sinistra, alto a destra, in basso a destra e in basso a sinistra: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)
            };
        }
    }
    ···
}

Questa matrice viene usata nel metodo seguente, che viene chiamato HitTest.This array is used in the following method, which is called HitTest. Il SKPoint parametro è un punto corrisponde a un tocco con un dito o un clic del mouse.The SKPoint parameter is a point corresponding to a finger touch or a mouse click. Il metodo restituisce un indice (0, 1, 2 o 3) corrispondente all'angolo che il puntatore del mouse o del dito manipolato, entro una distanza specificata dal radius parametro: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;
    }
    ···
}

Se il punto di tocco o mouse non è stato interno radius unità di un angolo, il metodo restituisce –1.If the touch or mouse point was not within radius units of any corner, the method returns –1.

Il metodo finale in CroppingRectangle viene chiamato MoveCorner, che viene chiamato in risposta a tocco o mouse lo spostamento.The final method in CroppingRectangle is called MoveCorner, which is called in response to touch or mouse movement. I due parametri indicano l'indice dell'angolo lo spostamento e la nuova posizione dell'angolo corrispondente.The two parameters indicate the index of the corner being moved, and the new location of that corner. Nella prima metà del metodo consente di regolare il rettangolo di ritaglio basato nella nuova posizione dell'angolo, ma sempre all'interno di maxRect, che rappresentano le dimensioni della bitmap.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. Questa logica inoltre prende in considerazione il MINIMUM campo per evitare di comprimere il rettangolo di ritaglio in nulla: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;
    }
}

La seconda metà del metodo regola per le proporzioni facoltativa.The second half of the method adjusts for the optional aspect ratio.

Tenere presente che tutti gli elementi di questa classe è espressa in unità di pixel.Keep in mind that everything in this class is in units of pixels.

Una vista di canvas solo per il ritaglioA canvas view just for cropping

Il CroppingRectangle classe riportato in precedenza viene usata per il PhotoCropperCanvasView classe che deriva da SKCanvasView.The CroppingRectangle class you've just seen is used by the PhotoCropperCanvasView class, which derives from SKCanvasView. Questa classe è responsabile della visualizzazione di bitmap e il rettangolo di ritaglio, nonché la gestione degli eventi di tocco o mouse per modificare il rettangolo di ritaglio.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.

Il PhotoCropperCanvasView costruttore richiede una bitmap.The PhotoCropperCanvasView constructor requires a bitmap. Le proporzioni è facoltativa.An aspect ratio is optional. Costruttore crea un'istanza di un oggetto di tipo CroppingRectangle basato su questa mappa di bit e le proporzioni e lo salva come un campo: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);
        ···
    }
    ···
}

Poiché questa classe deriva da SKCanvasView, non è necessario installare un gestore per il PaintSurface evento.Because this class derives from SKCanvasView, it doesn't need to install a handler for the PaintSurface event. Invece possibile eseguire l'override relativo OnPaintSurface (metodo).It can instead override its OnPaintSurface method. Il metodo consente di visualizzare la mappa di bit e Usa un paio di SKPaint salvati come campi per disegnare il rettangolo di ritaglio corrente: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);
    }
    ···
}

Il codice nel CroppingRectangle classe rettangolo di ritaglio si basa sulle dimensioni in pixel della bitmap.The code in the CroppingRectangle class bases the cropping rectangle on the pixel size of the bitmap. Tuttavia, la visualizzazione della bitmap dal PhotoCropperCanvasView classe viene ridimensionata in base alle dimensioni dell'area di visualizzazione.However, the display of the bitmap by the PhotoCropperCanvasView class is scaled based on the size of the display area. Il bitmapScaleMatrix calcolato nel OnPaintSurface eseguire l'override delle mappe dei pixel della bitmap per le dimensioni e la posizione della bitmap che viene visualizzato.The bitmapScaleMatrix calculated in the OnPaintSurface override maps from the bitmap pixels to the size and position of the bitmap as it is displayed. Questa matrice viene quindi utilizzata per trasformare il rettangolo di ritaglio in modo che possa essere visualizzato rispetto alla bitmap.This matrix is then used to transform the cropping rectangle so that it can be displayed relative to the bitmap.

L'ultima riga del OnPaintSurface override preleva l'inverso del bitmapScaleMatrix e lo salva come il inverseBitmapMatrix campo.The last line of the OnPaintSurface override takes the inverse of the bitmapScaleMatrix and saves it as the inverseBitmapMatrix field. Viene utilizzato per l'elaborazione di tocco.This is used for touch processing.

A TouchEffect viene creata un'istanza di oggetto come un campo e il costruttore ne allega un gestore per il TouchAction evento, ma il TouchEffect deve essere aggiunto al Effects raccolta del padre del SKCanvasViewderivativo, in modo che del richiede solo pochi i OnParentSet eseguire l'override: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));
    }
}

Gli eventi di tocco elaborato dal TouchAction gestore sono espressi in unità indipendenti dal dispositivo.The touch events processed by the TouchAction handler are in device-independent units. Questi innanzitutto elementi dovranno essere convertite in pixel usando il ConvertToPixel metodo nella parte inferiore della classe e quindi convertito in CroppingRectangle unità utilizzando 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.

Per Pressed gli eventi, il TouchAction chiamate del gestore di HitTest metodo CroppingRectangle.For Pressed events, the TouchAction handler calls the HitTest method of CroppingRectangle. Se il risultato è diverso da un indice –1, quindi uno degli angoli del rettangolo di ritaglio è in corso la modifica.If this returns an index other than –1, then one of the corners of the cropping rectangle is being manipulated. Che l'indice e un offset del punto di tocco effettivo nell'angolo viene archiviata in una TouchPoint dell'oggetto e quindi aggiungervi il touchPoints dizionario.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.

Per il Moved evento, il MoveCorner metodo CroppingRectangle viene chiamato per spostare l'angolo, con possibili modifiche per le proporzioni.For the Moved event, the MoveCorner method of CroppingRectangle is called to move the corner, with possible adjustments for the aspect ratio.

In qualsiasi momento, un programma tramite PhotoCropperCanvasView può accedere il CroppedBitmap proprietà.At any time, a program using PhotoCropperCanvasView can access the CroppedBitmap property. Questa proprietà Usa il Rect proprietà del CroppingRectangle per creare una nuova bitmap della dimensione ritagliata.This property uses the Rect property of the CroppingRectangle to create a new bitmap of the cropped size. La versione di DrawBitmap con origine e destinazione rettangoli estrae quindi un subset della bitmap originale: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;
        }
    }
    ···
}

La visualizzazione di canvas Ritaglia foto di hostingHosting the photo cropper canvas view

Con queste due classi che gestiscono la logica di ritaglio, il foto ritaglio pagina il SkiaSharpFormsDemos applicazione dispone di un impegno minimo per eseguire.With those two classes handling the cropping logic, the Photo Cropping page in the SkiaSharpFormsDemos application has very little work to do. Il file XAML crea un'istanza di un Grid all'host la PhotoCropperCanvasView e un eseguita pulsante: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>

Il PhotoCropperCanvasView non è possibile creare istanze nel file XAML perché richiede un parametro di tipo SKBitmap.The PhotoCropperCanvasView cannot be instantiated in the XAML file because it requires a parameter of type SKBitmap.

Al contrario, il PhotoCropperCanvasView viene creata un'istanza nel costruttore del file code-behind usando una delle bitmap di risorsa: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);
    }
}

È possibile quindi manipolare il rettangolo di ritaglio:The user can then manipulate the cropping rectangle:

Ritaglia 1 di fotoPhoto Cropper 1

Quando è stato definito un rettangolo di ritaglio ottimo, scegliere il pulsante .When a good cropping rectangle has been defined, click the Done button. Il Clicked gestore ottiene bitmap ritagliata dal CroppedBitmap proprietà di PhotoCropperCanvasViewe sostituisce tutto il contenuto della pagina con un nuovo SKCanvasView oggetto che consente di visualizzare questa bitmap ritagliata: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:

Ritaglia 2 di fotoPhoto Cropper 2

Provare a impostare il secondo argomento della PhotoCropperCanvasView a 1.78f (ad esempio):Try setting the second argument of PhotoCropperCanvasView to 1.78f (for example):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Si noterà il rettangolo di ritaglio limitato a un rapporto di aspetto 16-a-9 caratteristiche di HD.You'll see the cropping rectangle restricted to a 16-to-9 aspect ratio characteristic of high-definition television.

Suddivisione di una bitmap in riquadriDividing a bitmap into tiles

Una versione di xamarin. Forms del famoso puzzle di 14 o 15 è presente in 22 capitolo del libro creazione di App per dispositivi mobili con xamarin. Forms e possono essere scaricati come 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. Tuttavia, il puzzle diventa più divertente (e spesso più complesso) se si basa su un'immagine dalla propria libreria di foto.However, the puzzle becomes more fun (and often more challenging) when it is based on an image from your own photo library.

Questa versione del puzzle 14 o 15 fa parte del SkiaSharpFormsDemos dell'applicazione ed è costituito da una serie di pagine intitolata foto rompicapo.This version of the 14-15 puzzle is part of the SkiaSharpFormsDemos application, and consists of a series of pages titled Photo Puzzle.

Il PhotoPuzzlePage1.xaml file costituito da un 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>

Il file code-behind implementa una Clicked gestore che utilizza il IPhotoLibrary servizio di dipendenza per consentire all'utente di scegliere una foto dalla raccolta foto: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));
            }
        }
    }
}

Il metodo passa quindi al PhotoPuzzlePage2, passando al costruttore la mappa di bit selezionato.The method then navigates to PhotoPuzzlePage2, passing to the constuctor the selected bitmap.

È possibile che la foto selezionata dalla libreria non è orientata come veniva visualizzato nella raccolta foto, ma viene ruotato o capovolto.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. (Si tratta in particolare un problema con i dispositivi iOS). Per questo motivo, PhotoPuzzlePage2 consente di ruotare l'immagine da un orientamento desiderato.(This is particularly a problem with iOS devices.) For that reason, PhotoPuzzlePage2 allows you to rotate the image to a desired orientation. Il file XAML contiene tre pulsanti contrassegnati 90° a destra (vale a dire in senso orario), 90° sinistra (in senso antiorario), e eseguita.The XAML file contains three buttons labeled 90° Right (meaning clockwise), 90° Left (counterclockwise), and Done.

Il file code-behind implementa la logica di bitmap-rotazione illustrata nell'articolo creazione e la creazione su SkiaSharp Bitmaps .The code-behind file implements the bitmap-rotation logic shown in the article Creating and Drawing on SkiaSharp Bitmaps. L'utente può ruotare l'immagine di 90 gradi in senso orario o antiorario un numero qualsiasi di volte in cui: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));
    }
}

Quando l'utente fa clic il eseguite pulsante, il Clicked gestore passa a PhotoPuzzlePage3, passando la bitmap ruotata finale nel costruttore della pagina.When the user clicks the Done button, the Clicked handler navigates to PhotoPuzzlePage3, passing the final rotated bitmap in the page's constructor.

PhotoPuzzlePage3 consente la foto da ritagliare.PhotoPuzzlePage3 allows the photo to be cropped. Il programma richiede una bitmap per dividere in una griglia 4 per 4 riquadri quadrata.The program requires a square bitmap to divide into a 4-by-4 grid of tiles.

Il PhotoPuzzlePage3.xaml file contiene una Label, una Grid all'host il PhotoCropperCanvasViewe un altro eseguita pulsante: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>

Il file code-behind creare un'istanza di PhotoCropperCanvasView con la bitmap passata al costruttore.The code-behind file instantiates the PhotoCropperCanvasView with the bitmap passed to its constructor. Si noti che un valore 1 viene passato come secondo argomento per PhotoCropperCanvasView.Notice that a 1 is passed as the second argument to PhotoCropperCanvasView. Questo rapporto di 1 forza il rettangolo di ritaglio da un quadrato: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));
    }
}

Il gestore del pulsante Ottiene la larghezza e altezza della bitmap ritagliata (questi due valori devono essere lo stesso) e quindi si divide in 15 bitmap separate, ognuno dei quali è 1 e 4 la larghezza e altezza dell'originale .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. (L'ultima delle bitmap di 16 possibili non viene creato). Il DrawBitmap metodo con rettangolo di origine e di destinazione consente a una bitmap essere creata in base a subset di una bitmap di dimensioni maggiori.(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.

Conversione di bitmap di xamarin. FormsConverting to Xamarin.Forms bitmaps

Nel OnDoneButtonClicked metodo, la matrice creata per le 15 bitmap JE typu ImageSource :In the OnDoneButtonClicked method, the array created for the 15 bitmaps is of type ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource è il tipo di base di xamarin. Forms che incapsula una bitmap.ImageSource is the Xamarin.Forms base type that encapsulates a bitmap. Fortunatamente, SkiaSharp consente la conversione dalle bitmap di SkiaSharp in xamarin. Forms bitmap.Fortunately, SkiaSharp allows converting from SkiaSharp bitmaps to Xamarin.Forms bitmaps. Il SkiaSharp.Views.Forms assembly definisce un' SKBitmapImageSource classe che deriva da ImageSource ma possono essere creati come base un SkiaSharp SKBitmap oggetto.The SkiaSharp.Views.Forms assembly defines an SKBitmapImageSource class that derives from ImageSource but can be created based on a SkiaSharp SKBitmap object. SKBitmapImageSource definisce anche le conversioni tra SKBitmapImageSource e SKBitmape di come SKBitmap gli oggetti vengono archiviati in una matrice come le bitmap di 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;

Questa matrice delle bitmap viene passata come un costruttore PhotoPuzzlePage4.This array of bitmaps is passed as a constructor to PhotoPuzzlePage4. Tale pagina è completamente xamarin. Forms e non usa alcun SkiaSharp.That page is entirely Xamarin.Forms and doesn't use any SkiaSharp. È molto simile a XamagonXuzzle, pertanto non verrà descritta di seguito, ma visualizza la foto selezionata suddivisa in riquadri quadrati 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:

Foto 1 PuzzlePhoto Puzzle 1

Premere il Randomize una combinazione di pulsante backup di tutti i riquadri:Pressing the Randomize button mixes up all the tiles:

Foto di 2 PuzzlePhoto Puzzle 2

A questo punto è possibile inserirli nuovamente nell'ordine corretto.Now you can put them back in the correct order. Tutte le sezioni nella stessa riga o colonna sotto forma di quadrato vuoto possono essere toccate trasferendoli poi nel quadrato vuoto.Any tiles in the same row or column as the blank square can be tapped to move them into the blank square.