Zuschneiden von SkiaSharp-Bitmaps

Beispiel herunterladen Das Beispiel herunterladen

Im Artikel Erstellen und Zeichnen von SkiaSharp-Bitmaps wurde beschrieben, wie ein SKBitmap Objekt an einen SKCanvas Konstruktor übergeben werden kann. Jede Zeichnungsmethode, die für diesen Zeichenbereich aufgerufen wird, bewirkt, dass Grafiken in der Bitmap gerendert werden. Diese Zeichnungsmethoden umfassen DrawBitmap, was bedeutet, dass diese Technik das Übertragen eines Teils oder der gesamten Bitmap in eine andere Bitmap ermöglicht, möglicherweise mit angewendeten Transformationen.

Sie können diese Technik zum Zuschneiden einer Bitmap verwenden, indem Sie die DrawBitmap -Methode mit Quell- und Zielrechtecken aufrufen:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Anwendungen, die das Zuschneiden implementieren, bieten jedoch häufig eine Schnittstelle, über die der Benutzer das Zuschneiderechteck interaktiv auswählen kann:

Beispiel für zuschneidende

Dieser Artikel konzentriert sich auf diese Schnittstelle.

Kapseln des Zuschneiderechtecks

Es ist hilfreich, einen Teil der Zuschneidelogik in einer Klasse namens CroppingRectanglezu isolieren. Die Konstruktorparameter umfassen ein maximales Rechteck, das in der Regel die Größe der zuschneidenden Bitmap ist, und ein optionales Seitenverhältnis. Der Konstruktor definiert zuerst ein anfängliches Zuschneiderechteck, das er in der Rect -Eigenschaft des Typs SKRectöffentlich macht. Dieses anfängliche Zuschneiderechteck beträgt 80 % der Breite und Höhe des Bitmaprechtecks, wird jedoch angepasst, wenn ein Seitenverhältnis angegeben wird:

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

Eine nützliche Information, die ebenfalls verfügbar macht, CroppingRectangle ist ein Array von SKPoint Werten, die den vier Ecken des Zuschneiderechtecks in der Reihenfolge oben links, oben rechts, unten rechts und unten links entsprechen:

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

Dieses Array wird in der folgenden Methode verwendet, die als bezeichnet HitTestwird. Der SKPoint Parameter ist ein Punkt, der einer Fingereingabe oder einem Mausklick entspricht. Die -Methode gibt einen Index (0, 1, 2 oder 3) zurück, der der Ecke entspricht, die der Finger- oder Mauszeiger berührt hat, innerhalb einer entfernung, die radius vom Parameter angegeben wird:

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

Wenn sich der Touch- oder Mauspunkt nicht innerhalb radius der Einheiten einer Ecke befand, gibt die Methode –1 zurück.

Die letzte Methode in CroppingRectangle heißt MoveCorner, die als Reaktion auf Touch- oder Mausbewegungen aufgerufen wird. Die beiden Parameter geben den Index der zu verschiebenden Ecke und die neue Position dieser Ecke an. Die erste Hälfte der Methode passt das Zuschneiderechteck basierend auf der neuen Position der Ecke an, jedoch immer innerhalb der Grenzen von maxRect, was der Größe der Bitmap entspricht. Diese Logik berücksichtigt auch das MINIMUM Feld, um zu vermeiden, dass das Zuschneiderechteck in nichts zusammenbricht:

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

Die zweite Hälfte der Methode wird für das optionale Seitenverhältnis angepasst.

Beachten Sie, dass sich alles in dieser Klasse in Pixeleinheiten befindet.

Eine Canvasansicht nur zum Zuschneiden

Die CroppingRectangle soeben gesehene Klasse wird von der PhotoCropperCanvasView -Klasse verwendet, die von SKCanvasViewabgeleitet wird. Diese Klasse ist für die Anzeige der Bitmap und des Zuschneiderechtecks sowie für die Behandlung von Touch- oder Mausereignissen zum Ändern des Zuschneiderechtecks zuständig.

Der PhotoCropperCanvasView Konstruktor erfordert eine Bitmap. Ein Seitenverhältnis ist optional. Der Konstruktor instanziiert ein Objekt vom Typ CroppingRectangle basierend auf diesem Bitmap- und Seitenverhältnis und speichert es als Feld:

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

Da diese Klasse von SKCanvasViewabgeleitet ist, muss kein Handler für das PaintSurface -Ereignis installiert werden. Stattdessen kann die -Methode überschrieben werden OnPaintSurface . Die -Methode zeigt die Bitmap an und verwendet einige objekte SKPaint , die als Felder gespeichert sind, um das aktuelle Zuschneiderechteck zu zeichnen:

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

Der Code in der CroppingRectangle -Klasse basiert das Zuschneiderechteck auf der Pixelgröße der Bitmap. Die Anzeige der Bitmap durch die PhotoCropperCanvasView -Klasse wird jedoch basierend auf der Größe des Anzeigebereichs skaliert. Die bitmapScaleMatrix in der OnPaintSurface Überschreibung berechnete ordnet die Bitmappixel der Größe und Position der Bitmap zu, während sie angezeigt wird. Diese Matrix wird dann verwendet, um das Zuschneiderechteck so zu transformieren, dass es relativ zur Bitmap angezeigt werden kann.

Die letzte Zeile der OnPaintSurface Außerkraftsetzung nimmt die Umgekehrte von bitmapScaleMatrix und speichert sie als Feld inverseBitmapMatrix . Dies wird für die Verarbeitung von Toucheingaben verwendet.

Ein TouchEffect -Objekt wird als Feld instanziiert, und der Konstruktor fügt einen Handler an das TouchAction -Ereignis an, aber das TouchEffect muss der Effects Auflistung des übergeordnetenSKCanvasView Derivat hinzugefügt werden, damit dies in der OnParentSet Außerkraftsetzung erfolgt:

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

Die vom TouchAction Handler verarbeiteten Touchereignisse befinden sich in geräteunabhängigen Einheiten. Diese müssen zunächst mithilfe der ConvertToPixel -Methode am unteren Rand der -Klasse in Pixel konvertiert und dann mithilfe inverseBitmapMatrixvon in Einheiten konvertiert CroppingRectangle werden.

Für Pressed Ereignisse ruft der TouchAction Handler die HitTest -Methode von auf CroppingRectangle. Wenn dadurch ein anderer Index als –1 zurückgegeben wird, wird eine der Ecken des Zuschneiderechtecks bearbeitet. Dieser Index und ein Offset des tatsächlichen Berührungspunkts von der Ecke werden in einem TouchPoint -Objekt gespeichert und dem touchPoints Wörterbuch hinzugefügt.

Für das Moved -Ereignis wird die MoveCorner -Methode von CroppingRectangle aufgerufen, um die Ecke mit möglichen Anpassungen für das Seitenverhältnis zu verschieben.

Ein Programm, das verwendet PhotoCropperCanvasView , kann jederzeit auf die CroppedBitmap -Eigenschaft zugreifen. Diese Eigenschaft verwendet die Rect -Eigenschaft von CroppingRectangle , um eine neue Bitmap der zugeschnittenen Größe zu erstellen. Die Version von DrawBitmap mit Ziel- und Quellrechtecke extrahiert dann eine Teilmenge der ursprünglichen 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;
        }
    }
    ···
}

Hosten der Fotozuschneidebereichsansicht

Da diese beiden Klassen die Zuschneidelogik behandeln, hat die Seite Fotozuschneidevorgänge in der Anwendung SkiaSharpFormsDemos nur sehr wenig Arbeit zu erledigen. Die XAML-Datei instanziiert eine Grid , um die PhotoCropperCanvasView Schaltflächen und die Schaltflächen Fertig zu hosten :

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

Kann PhotoCropperCanvasView in der XAML-Datei nicht instanziiert werden, da ein Parameter vom Typ SKBitmaperforderlich ist.

Stattdessen wird im PhotoCropperCanvasView Konstruktor der CodeBehind-Datei mithilfe einer der Ressourcenbits instanziiert:

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

Der Benutzer kann dann das Zuschneiderechteck bearbeiten:

Photo Cropper 1

Wenn ein gutes Zuschneiderechteck definiert wurde, klicken Sie auf die Schaltfläche Fertig . Der Clicked Handler ruft die zugeschnittene Bitmap aus der CroppedBitmap -Eigenschaft von PhotoCropperCanvasViewab und ersetzt den gesamten Inhalt der Seite durch ein neues SKCanvasView Objekt, das diese zugeschnittene Bitmap anzeigt:

Photo Cropper 2

Versuchen Sie, das zweite Argument von PhotoCropperCanvasView auf 1,78f festzulegen (z. B.):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Sie sehen das Zuschneiderechteck, das auf ein Seitenverhältnis von 16 zu 9 beschränkt ist, das für hochauflösendes Fernsehen charakteristisch ist.

Aufteilen einer Bitmap in Kacheln

Eine Xamarin.Forms Version des berühmten 14-15-Puzzles erschien in Kapitel 22 des Buches Creating Mobile Apps with Xamarin.Forms und kann als XamagonXuzzle heruntergeladen werden. Allerdings wird das Puzzle mehr Spaß (und oft anspruchsvoller), wenn es auf einem Bild aus Ihrer eigenen Fotobibliothek basiert.

Diese Version des 14-15-Puzzles ist Teil der SkiaSharpFormsDemos-Anwendung und besteht aus einer Reihe von Seiten mit dem Titel Photo Puzzle.

Die Datei PhotoPuzzlePage1.xaml besteht aus :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>

Die CodeBehind-Datei implementiert einen Clicked Handler, der den IPhotoLibrary Abhängigkeitsdienst verwendet, um dem Benutzer die Auswahl eines Fotos aus der Fotobibliothek zu ermöglichen:

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

Die -Methode navigiert dann zu PhotoPuzzlePage2, und übergibt an den Constuctor die ausgewählte Bitmap.

Es ist möglich, dass das aus der Bibliothek ausgewählte Foto nicht so ausgerichtet ist, wie es in der Fotobibliothek angezeigt wurde, sondern gedreht oder auf den Kopf gestellt wird. (Dies ist insbesondere bei iOS-Geräten ein Problem.) Aus diesem Grund PhotoPuzzlePage2 können Sie das Bild in eine gewünschte Ausrichtung drehen. Die XAML-Datei enthält drei Schaltflächen mit der Bezeichnung 90° Rechts (d. h. im Uhrzeigersinn), 90° Links (gegen den Uhrzeigersinn) und Fertig.

Die CodeBehind-Datei implementiert die Bitmapdrehungslogik, die im Artikel Erstellen und Zeichnen von SkiaSharp-Bitmaps gezeigt wird. Der Benutzer kann das Bild beliebig oft um 90 Grad im oder gegen den Uhrzeigersinn drehen:

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

Wenn der Benutzer auf die Schaltfläche Fertig klickt, navigiert der Clicked Handler zu PhotoPuzzlePage3und übergibt die endgültige gedrehte Bitmap im Konstruktor der Seite.

PhotoPuzzlePage3 ermöglicht das Zuschneiden des Fotos. Das Programm erfordert eine quadratische Bitmap, um in ein 4-mal-4-Raster von Kacheln zu unterteilen.

Die Datei PhotoPuzzlePage3.xaml enthält eine Label, eine Grid zum Hosten von PhotoCropperCanvasViewund eine weitere Schaltfläche Fertig :

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

Die CodeBehind-Datei instanziiert die PhotoCropperCanvasView mit der An ihren Konstruktor übergebenen Bitmap. Beachten Sie, dass eine 1 als zweites Argument an PhotoCropperCanvasViewübergeben wird. Dieses Seitenverhältnis von 1 erzwingt, dass das Zuschneiderechteck ein Quadrat ist:

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

Der Schaltflächenhandler fertig ruft die Breite und Höhe der zugeschnittenen Bitmap ab (diese beiden Werte sollten identisch sein) und teilt sie dann in 15 separate Bitmaps, von denen jedes 1/4 die Breite und Höhe des Originals aufweist. (Die letzte der möglichen 16 Bitmaps wird nicht erstellt.) Die DrawBitmap -Methode mit Quell- und Zielrechteck ermöglicht das Erstellen einer Bitmap basierend auf einer Teilmenge einer größeren Bitmap.

Konvertieren in Xamarin.Forms Bitmaps

In der OnDoneButtonClicked -Methode ist das Array, das für die 15 Bitmaps erstellt wurde, vom Typ ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource ist der Xamarin.Forms Basistyp, der eine Bitmap kapselt. Glücklicherweise ermöglicht SkiaSharp die Konvertierung von SkiaSharp-Bitmaps in Xamarin.Forms Bitmaps. Die SkiaSharp.Views.Forms-Assembly definiert eine SKBitmapImageSource Klasse, die von ImageSource abgeleitet wird, aber basierend auf einem SkiaSharp-Objekt SKBitmap erstellt werden kann. SKBitmapImageSource definiert sogar Konvertierungen zwischen SKBitmapImageSource und SKBitmap, und so SKBitmap werden Objekte in einem Array als Xamarin.Forms Bitmaps gespeichert:

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

Dieses Array von Bitmaps wird als Konstruktor an PhotoPuzzlePage4übergeben. Diese Seite ist vollständig Xamarin.Forms und verwendet keine SkiaSharp. Es ist XamagonXuzzle sehr ähnlich, daher wird es hier nicht beschrieben, aber es zeigt Ihr ausgewähltes Foto in 15 quadratische Kacheln unterteilt:

Fotopuzzle 1

Durch drücken der Schaltfläche Randomize werden alle Kacheln durcheinander gemischt:

Fotopuzzle 2

Jetzt können Sie sie wieder in der richtigen Reihenfolge platzieren. Alle Kacheln in derselben Zeile oder Spalte wie das leere Quadrat können angetippt werden, um sie in das leere Quadrat zu verschieben.