Oříznutí rastrových obrázků SkiaSharp

Download Sample Stažení ukázky

Článek Vytváření a kreslení SkiaSharp Bitmaps popisuje, jak SKBitmap lze objekt předat konstruktoru SKCanvas . Jakákoli metoda kreslení volaná na plátně způsobí vykreslení grafiky na rastrovém obrázku. Tyto metody kreslení zahrnují DrawBitmap, což znamená, že tato technika umožňuje přenést část nebo všechny rastrové obrázky do jiného rastrového obrázku, například s použitými transformacemi.

Tuto techniku můžete použít k oříznutí bitmapy zavoláním DrawBitmap metody se zdrojovými a cílovými obdélníky:

canvas.DrawBitmap(bitmap, sourceRect, destRect);

Aplikace, které implementují oříznutí, ale často poskytují rozhraní pro uživatele, aby interaktivně vybral obdélník oříznutí:

Cropping Sample

Tento článek se zaměřuje na toto rozhraní.

Zapouzdření obdélníku oříznutí

Je užitečné izolovat některé logiky oříznutí ve třídě s názvem CroppingRectangle. Parametry konstruktoru obsahují maximální obdélník, což je obecně velikost rastrového obrázku oříznutého obrázku a volitelný poměr stran. Konstruktor nejprve definuje počáteční obdélník oříznutí, který zpřístupňuje ve Rect vlastnosti typu SKRect. Tento počáteční obdélník oříznutí je 80 % šířky a výšky rastrového obdélníku, ale je upraven, pokud je zadán poměr stran:

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

Jednou z užitečných informací, které CroppingRectangle jsou k dispozici, je matice SKPoint hodnot odpovídající čtyřem rohům obdélníku oříznutí v pravém horním rohu, vpravo nahoře, vpravo dole a v levém dolním rohu:

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

Toto pole se používá v následující metodě, která se nazývá HitTest. Parametr SKPoint je bod odpovídající dotyku prstem nebo kliknutí myší. Metoda vrátí index (0, 1, 2 nebo 3) odpovídající rohu, na který se prst nebo ukazatel myši dotýkal, ve vzdálenosti zadané parametrem radius :

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

Pokud touch nebo myš nebyl v radius jednotkách žádného rohu, vrátí metoda –1.

Poslední metoda je CroppingRectangle volána MoveCorner, která je volána v reakci na pohyb dotyku nebo myši. Dva parametry označují index přesunu rohu a nové umístění tohoto rohu. První polovina metody upraví obdélník oříznutí na základě nového umístění rohu, ale vždy v mezích maxRect, což je velikost rastrového obrázku. Tato logika také bere v úvahu MINIMUM pole, aby se zabránilo sbalení obdélníku oříznutí do nic:

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

Druhá polovina metody se přizpůsobí volitelnému poměru stran.

Mějte na paměti, že všechno v této třídě je v jednotkách pixelů.

Zobrazení plátna jen pro oříznutí

Třída CroppingRectangle , kterou jste právě viděli, je používána PhotoCropperCanvasView třídou, která je odvozena od SKCanvasView. Tato třída zodpovídá za zobrazení rastrového obrázku a obdélníku oříznutí a také zpracování událostí dotykového ovládání nebo myši za změnu obdélníku oříznutí.

Konstruktor PhotoCropperCanvasView vyžaduje rastrový obrázek. Poměr stran je volitelný. Konstruktor vytvoří instanci objektu typu CroppingRectangle na základě tohoto rastrového obrázku a poměru stran a uloží ho jako pole:

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

Vzhledem k tomu, že tato třída je odvozena od SKCanvasView, nemusí instalovat obslužnou rutinu PaintSurface události. Místo toho může přepsat svou OnPaintSurface metodu. Metoda zobrazí rastrový obrázek a používá několik objektů uložených SKPaint jako pole k vykreslení aktuálního obdélníku oříznutí:

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

Kód ve CroppingRectangle třídě zakřižuje obdélník oříznutí na velikosti pixelu rastrového obrázku. Zobrazení rastrového obrázku podle PhotoCropperCanvasView třídy se však škáluje na základě velikosti oblasti zobrazení. Počítané bitmapScaleMatrix v OnPaintSurface mapách přepsání z rastrových pixelů na velikost a pozici rastrového obrázku, jak je zobrazen. Tato matice se pak použije k transformaci obdélníku oříznutí, aby bylo možné jej zobrazit vzhledem k bitmapě.

Poslední řádek OnPaintSurface přepsání převezme inverzní funkci bitmapScaleMatrix a uloží ho jako inverseBitmapMatrix pole. Používá se k dotykovému zpracování.

Objekt TouchEffect se vytvoří instance jako pole a konstruktor připojí obslužnou rutinu TouchAction k události, ale TouchEffect je nutné ho přidat do Effects kolekce nadřazeného objektu derivátu SKCanvasView , aby to bylo provedeno v OnParentSet přepsání:

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

Dotykové události zpracovávané obslužnou rutinou TouchAction jsou v jednotkách nezávislých na zařízení. Nejprve je potřeba převést na pixely pomocí ConvertToPixel metody v dolní části třídy a pak je převést na CroppingRectangle jednotky pomocí inverseBitmapMatrix.

U Pressed událostí obslužná rutina TouchAction volá metodu CroppingRectangleHitTest . Pokud se vrátí jiný index než -1, pak se pracuje s jedním z rohů obdélníku oříznutí. Tento index a posun skutečného dotykového bodu z rohu jsou uloženy v objektu TouchPoint a přidány do slovníku touchPoints .

Moved U události MoveCorner je volána metoda CroppingRectangle přesunutí rohu s možnými úpravami poměru stran.

Program, který používá PhotoCropperCanvasView tuto vlastnost, má kdykoli přístup k CroppedBitmap vlastnosti. Tato vlastnost používá Rect vlastnost objektu CroppingRectangle k vytvoření nového rastrového obrázku oříznuté velikosti. Verze DrawBitmap s cílovými a zdrojovými obdélníky pak extrahuje podmnožinu původního rastrového obrázku:

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

Hostování zobrazení plátna pro oříznutí fotek

S těmito dvěma třídami, které zpracovávají logiku oříznutí, má stránka Oříznutí fotografie v aplikaci SkiaSharpFormsDemos velmi málo práce. Soubor XAML vytvoří Grid instanci pro hostování PhotoCropperCanvasView a tlačítko Hotovo :

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

V PhotoCropperCanvasView souboru XAML nelze vytvořit instanci, protože vyžaduje parametr typu SKBitmap.

Místo toho se vytvoří PhotoCropperCanvasView instance v konstruktoru souboru kódu za použitím některého z rastrových obrázků prostředků:

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

Uživatel pak může manipulovat s obdélníkem oříznutí:

Photo Cropper 1

Po definování dobrého obdélníku oříznutí klikněte na tlačítko Hotovo . Obslužná Clicked rutina získá oříznutý rastrový obrázek z CroppedBitmap vlastnosti PhotoCropperCanvasViewa nahradí veškerý obsah stránky novým SKCanvasView objektem, který zobrazí tento oříznutý rastrový obrázek:

Photo Cropper 2

Zkuste nastavit druhý argument PhotoCropperCanvasView 1,78f (například):

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

Zobrazí se obdélník oříznutí omezený na poměr stran 16 až 9, který je charakteristické pro televizi s vysokým rozlišením.

Rozdělení rastrového obrázku na dlaždice

Verze Xamarin.Forms slavné 14-15 puzzle se objevila v kapitole 22 knihy Vytváření mobilních aplikací sXamarin.Formsa lze stáhnout jako XamagonXuzzle. Puzzle se ale stává zábavnější (a často náročnější), když je založen na obrázku z vlastní knihovny fotografií.

Tato verze 14-15 puzzle je součástí aplikace SkiaSharpFormsDemos a skládá se z řady stránek s názvem Photo Puzzle.

Soubor PhotoPuzzlePage1.xaml se skládá z 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>

Soubor s kódem implementuje obslužnou rutinu Clicked , která používá IPhotoLibrary službu závislostí, aby uživatel vybral fotku z knihovny fotografií:

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

Metoda pak přejde na PhotoPuzzlePage2, předat constuctor vybrané bitmapy.

Je možné, že fotka vybraná z knihovny není orientována tak, jak se zobrazuje v knihovně fotek, ale otočí se nebo vzhůru nohama. (Jedná se zejména o problém se zařízeními s iOSem.) Z tohoto důvodu PhotoPuzzlePage2 můžete obrázek otočit na požadovanou orientaci. Soubor XAML obsahuje tři tlačítka označená jako 90° vpravo (tj. ve směru hodinových ručiček), 90° vlevo (proti směru hodinových ručiček) a Hotovo.

Soubor s kódem implementuje logiku rastrového otočení znázorněnou v článku Vytváření a kreslení na skiaSharp Bitmaps. Uživatel může obrázek otočit o 90 stupňů po směru hodinových ručiček nebo proti směru hodinových ručiček libovolný počet:

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

Když uživatel klikne na tlačítko Hotovo , Clicked obslužná rutina přejde na PhotoPuzzlePage3, předá konečný otočený rastrový obrázek v konstruktoru stránky.

PhotoPuzzlePage3 umožňuje oříznout fotku. Program vyžaduje čtvercový rastrový obrázek, který se rozdělí na mřížku 4 po 4 dlaždicích.

Soubor PhotoPuzzlePage3.xaml obsahuje soubor , který Grid hostuje LabelPhotoCropperCanvasViewa další tlačítko Hotovo:

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

Soubor kódu vytvoří instanci PhotoCropperCanvasView rastrového obrázku předaného jeho konstruktoru. Všimněte si, že hodnota 1 se předává jako druhý argument PhotoCropperCanvasView. Tento poměr stran 1 vynutí obdélník oříznutí jako čtverec:

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

Obslužná rutina tlačítka Hotovo získá šířku a výšku oříznutého rastrového obrázku (tyto dvě hodnoty by měly být stejné) a pak ji rozdělí na 15 samostatných rastrových obrázků, z nichž každá je 1/4 šířky a výšky původního obrázku. (Poslední z možných 16 rastrových obrázků není vytvořen.) Metoda DrawBitmap se zdrojovým a cílovým obdélníkem umožňuje vytvořit rastrový obrázek na základě podmnožina většího rastrového obrázku.

Převod na Xamarin.Forms rastrové obrázky

OnDoneButtonClicked V metodě je pole vytvořené pro 15 rastrových obrázků typuImageSource:

ImageSource[] imgSources = new ImageSource[15];

ImageSourceXamarin.Forms je základní typ, který zapouzdřuje rastrový obrázek. Naštěstí SkiaSharp umožňuje převádět z rastrových obrázků SkiaSharp na Xamarin.Forms rastrové obrázky. Sestavení SkiaSharp.Views.Forms definuje SKBitmapImageSource třídu, která je odvozena, ImageSource ale lze ji vytvořit na základě objektu SkiaSharp SKBitmap . SKBitmapImageSource dokonce definuje převody mezi SKBitmapImageSource objekty a SKBitmap, a to je způsob, jakým SKBitmap jsou objekty uloženy v poli jako Xamarin.Forms rastrové obrázky:

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

Toto pole rastrových obrázků je předáno jako konstruktor do PhotoPuzzlePage4. Tato stránka je zcela Xamarin.Forms a nepoužívá žádné SkiaSharp. Je velmi podobný XamagonXuzzle, takže se zde nebude popisovat, ale zobrazí vybranou fotku rozdělenou na 15 čtvercových dlaždic:

Photo Puzzle 1

Stisknutím tlačítka Randomize se všechny dlaždice promíchají:

Photo Puzzle 2

Teď je můžete vrátit do správného pořadí. Všechny dlaždice ve stejném řádku nebo sloupci jako prázdný čtverec je možné klepnout a přesunout je do prázdného čtverečku.