Freigeben über


Zuschneiden von SkiaSharp-Bitmaps

Im Artikel "Creating and Drawing SkiaSharp Bitmaps " wird 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 Zeichenmethoden enthalten DrawBitmap, was bedeutet, dass diese Technik das Übertragen eines Teils oder aller Bitmaps zu einer anderen Bitmap ermöglicht, z. B. 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 zuschneiden implementieren, bieten dem Benutzer jedoch häufig eine Benutzeroberfläche, um das Zuschneiderechteck interaktiv auszuwählen:

Zuschneiden des Beispiels

Dieser Artikel konzentriert sich auf diese Schnittstelle.

Kapselung des Zuschneiderechtecks

Es ist hilfreich, einige der Zuschneidelogik in einer Klasse mit dem Namen CroppingRectanglezu isolieren. Die Konstruktorparameter enthalten ein maximales Rechteck, das im Allgemeinen die Größe der zuzuschneidenden Bitmap und ein optionales Seitenverhältnis darstellt. Der Konstruktor definiert zuerst ein anfängliches Zuschneiderechteck, das in der Rect Eigenschaft des Typs SKRectöffentlich wird. 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; }
    ···
}

Ein nützliches Informationselement, das auch zur Verfügung stellt, 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 aufgerufen 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 eines Abstands des radius Parameters:

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 Fingereingabe- oder Mauspunkt nicht innerhalb radius einer Ecke befindet, gibt die Methode "-1" zurück.

Die letzte Methode CroppingRectangle wird aufgerufen 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, aber immer innerhalb der Grenzen maxRectder Bitmap an. Diese Logik berücksichtigt auch das MINIMUM Feld, um zu vermeiden, dass das Zuschneiderechteck in nichts unterteilt wird:

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 passt sich für das optionale Seitenverhältnis an.

Denken Sie daran, dass sich alles in dieser Klasse in Pixeleinheiten befindet.

Eine Canvasansicht nur zum Zuschneiden

Die CroppingRectangle gerade gesehene Klasse wird von der PhotoCropperCanvasView Klasse verwendet, die von SKCanvasView. 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 verantwortlich.

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 SKCanvasView abgeleitet ist, muss kein Handler für das PaintSurface Ereignis installiert werden. Es kann stattdessen seine OnPaintSurface Methode überschreiben. Die Methode zeigt die Bitmap an und verwendet einige SKPaint Objekte, 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 auf dem 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 Außerkraftsetzung berechnete Zuordnung von Bitmappixeln zur Größe und Position der Bitmap, wie sie angezeigt wird. Diese Matrix wird dann verwendet, um das Zuschneiderechteck zu transformieren, sodass es relativ zur Bitmap angezeigt werden kann.

Die letzte Zeile der OnPaintSurface Überschreibung übernimmt die Umkehrung des bitmapScaleMatrix Felds und speichert sie als inverseBitmapMatrix Feld. Dies wird für die Touchverarbeitung verwendet.

Ein TouchEffect Objekt wird als Feld instanziiert, und der Konstruktor fügt einen Handler an das TouchAction Ereignis an, muss jedoch TouchEffect der Effects Auflistung des übergeordneten Elements des SKCanvasView Abgeleiteten hinzugefügt werden, sodass 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 zuerst mithilfe der ConvertToPixel Methode am unteren Rand der Klasse in Pixel konvertiert und dann mithilfe inverseBitmapMatrixvon Einheiten konvertiert CroppingRectangle werden.

Für Pressed Ereignisse ruft der TouchAction Handler die HitTest Methode von CroppingRectangle. Wenn dieser einen anderen Index als -1 zurückgibt, 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 CroppingRectangle aufgerufen, um die Ecke zu verschieben, mit möglichen Anpassungen für das Seitenverhältnis.

Jederzeit kann ein Programm, das PhotoCropperCanvasView verwendet wird, auf die CroppedBitmap Eigenschaft zugreifen. Diese Eigenschaft verwendet die Rect Eigenschaft der CroppingRectangle , um eine neue Bitmap der zugeschnittenen Größe zu erstellen. Die Version mit DrawBitmap Ziel- und Quellrechtecken 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 Fotozuschnitt-Canvasansicht

Mit diesen beiden Klassen, die die Zuschneidelogik behandeln, hat die Seite "Fotozuschneiden " in der Beispielanwendung sehr wenig Arbeit. Die XAML-Datei instanziiert eine Grid zum Hosten der PhotoCropperCanvasView Schaltfläche "Fertig" und " Fertig ":

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

Die PhotoCropperCanvasView Instanziierung in der XAML-Datei ist nicht möglich, da ein Parameter vom Typ SKBitmaperforderlich ist.

Stattdessen wird die PhotoCropperCanvasView Instanziierung im Konstruktor der CodeBehind-Datei mithilfe einer der Ressourcenbitmaps 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:

Fotozuschneide 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 PhotoCropperCanvasViewund ersetzt den gesamten Inhalt der Seite durch ein neues SKCanvasView Objekt, das diese zugeschnittene Bitmap anzeigt:

Fotozuschneide 2

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

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Das zuschneidende Rechteck ist auf ein Seitenverhältnis von 16 bis 9 beschränkt.

Aufteilen einer Bitmap in Kacheln

Eine Xamarin.Forms Version des berühmten 14-15 Puzzles erschien in Kapitel 22 des Buches Creating Mobile Apps mitXamarin.Formsund kann als XamagonXuzzle heruntergeladen werden. Das Puzzle wird jedoch lustiger (und oft schwieriger), wenn es auf einem Bild aus Ihrer eigenen Fotobibliothek basiert.

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

Die Datei "PhotoPuzzlePage1.xaml " besteht aus einem 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 Konstuctor 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 besonders ein Problem mit iOS-Geräten.) 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 (im Uhrzeigersinn), 90° Links (gegen den Uhrzeigersinn) und Fertig.

Die CodeBehind-Datei implementiert die Bitmapdrehungslogik, die im Artikel Erstellen und Zeichnen auf SkiaSharp Bitmaps gezeigt wird. Der Benutzer kann das Bild um 90 Grad im Uhrzeigersinn oder im Uhrzeigersinn beliebig oft 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 PhotoPuzzlePage3der letzten gedrehten Bitmap im Konstruktor der Seite.

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

Die Datei "PhotoPuzzlePage3.xaml " enthält eine Label, eine Grid zum Hosten der PhotoCropperCanvasViewSchaltfläche und 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 Bitmap, die an den Konstruktor übergeben wurde. Beachten Sie, dass ein 1 als zweites Argument übergeben wird.PhotoCropperCanvasView Dieses Seitenverhältnis von 1 erzwingt das Zuschneiderechteck als Quadrat:

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 auf, von denen jede 1/4 die Breite und Höhe des Originals ist. (Die letzte der möglichen 16 Bitmaps wird nicht erstellt.) Mit der DrawBitmap Methode mit Quell- und Zielrechteck kann eine Bitmap basierend auf einer Teilmenge einer größeren Bitmap erstellt werden.

Konvertieren in Xamarin.Forms Bitmaps

In der OnDoneButtonClicked Methode ist das für die 15 Bitmaps erstellte Array vom Typ ImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSource ist der Basistyp, der Xamarin.Forms eine Bitmap kapselt. Glücklicherweise ermöglicht SkiaSharp das Konvertieren von SkiaSharp-Bitmaps in Xamarin.Forms Bitmaps. Die SkiaSharp.Views.Forms-Assembly definiert eine SKBitmapImageSource Klasse, die von ImageSource der 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 PhotoPuzzlePage4an übergeben. Diese Seite ist vollständig Xamarin.Forms und verwendet keinen SkiaSharp. Es ist sehr ähnlich wie XamagonXuzzle, daher wird es hier nicht beschrieben, aber es zeigt Ihr ausgewähltes Foto, das in 15 quadratische Kacheln unterteilt ist:

Foto puzzle 1

Durch Drücken der Schaltfläche "Randomize " werden alle Kacheln vermischt:

Foto Puzzle 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.