SkiaSharp ビットマップのトリミング

Download Sampleサンプルのダウンロード

記事「SkiaSharp ビットマップの作成と描画」では、SKBitmap オブジェクトを SKCanvas コンストラクターに渡す方法について説明しました。 そのキャンバス上で呼び出された描画メソッドにより、グラフィックスがビットマップ上にレンダリングされます。 これらの描画メソッドには DrawBitmap が含まれます。この手法では、1 つのビットマップの一部または全体を別のビットマップに転送でき、変換が適用されることもあります。

この手法を使用してビットマップをトリミングする場合は、ソースとターゲットの四角形を指定して DrawBitmap メソッドを呼び出します。

canvas.DrawBitmap(bitmap, sourceRect, destRect);

ただし、トリミングを実装するアプリケーションには、ユーザーがトリミングする四角形を対話的に選択できるインターフェイスが用意されていることがよくあります。

Cropping Sample

この記事では、このインターフェイスを重点的に説明します。

トリミング四角形のカプセル化

トリミング ロジックの一部を CroppingRectangle という名前のクラスに分離すると便利です。 コンストラクター パラメーターには、最大の四角形 (通常はトリミングされるビットマップのサイズ) とオプションの縦横比が含まれます。 コンストラクターは、最初にトリミングする初期の四角形を定義し、SKRect 型の Rect プロパティでパブリックになります。 この初期のトリミング四角形は、その幅と高さがビットマップ四角形の 80% になりますが、縦横比が指定されている場合は調整されます。

class CroppingRectangle
{
    ···
    SKRect maxRect;             // generally the size of the bitmap
    float? aspectRatio;

    public CroppingRectangle(SKRect maxRect, float? aspectRatio = null)
    {
        this.maxRect = maxRect;
        this.aspectRatio = aspectRatio;

        // Set initial cropping rectangle
        Rect = new SKRect(0.9f * maxRect.Left + 0.1f * maxRect.Right,
                          0.9f * maxRect.Top + 0.1f * maxRect.Bottom,
                          0.1f * maxRect.Left + 0.9f * maxRect.Right,
                          0.1f * maxRect.Top + 0.9f * maxRect.Bottom);

        // Adjust for aspect ratio
        if (aspectRatio.HasValue)
        {
            SKRect rect = Rect;
            float aspect = aspectRatio.Value;

            if (rect.Width > aspect * rect.Height)
            {
                float width = aspect * rect.Height;
                rect.Left = (maxRect.Width - width) / 2;
                rect.Right = rect.Left + width;
            }
            else
            {
                float height = rect.Width / aspect;
                rect.Top = (maxRect.Height - height) / 2;
                rect.Bottom = rect.Top + height;
            }

            Rect = rect;
        }
    }

    public SKRect Rect { set; get; }
    ···
}

CroppingRectangle で使用できるようになる便利な情報の 1 つに、トリミング四角形の四隅に相当する SKPoint 値の配列があり、左上、右上、右下、左下の順になっています。

class CroppingRectangle
{
    ···
    public SKPoint[] Corners
    {
        get
        {
            return new SKPoint[]
            {
                new SKPoint(Rect.Left, Rect.Top),
                new SKPoint(Rect.Right, Rect.Top),
                new SKPoint(Rect.Right, Rect.Bottom),
                new SKPoint(Rect.Left, Rect.Bottom)
            };
        }
    }
    ···
}

この配列は HitTest と呼ばれる次のメソッドで使用されます。 SKPoint パラメーターは、指によるタッチまたはマウス クリックに対応するポイントです。 このメソッドは、radius パラメーターで指定された距離内で指またはマウス ポインターがタッチした隅に対応するインデックス (0、1、2、または 3) を返します。

class CroppingRectangle
{
    ···
    public int HitTest(SKPoint point, float radius)
    {
        SKPoint[] corners = Corners;

        for (int index = 0; index < corners.Length; index++)
        {
            SKPoint diff = point - corners[index];

            if ((float)Math.Sqrt(diff.X * diff.X + diff.Y * diff.Y) < radius)
            {
                return index;
            }
        }

        return -1;
    }
    ···
}

タッチまたはマウスのポイントが任意の隅の radius 単位以内でなかった場合、メソッドは -1 を返します。

CroppingRectangle の最後のメソッドは MoveCorner と呼ばれ、タッチまたはマウスの動きに応じて呼び出されます。 2 つのパラメーターは、移動する隅のインデックスと、その隅の新しい位置を示します。 メソッドの前半では、隅の新しい位置に基づいてトリミング四角形が調整されますが、常にビットマップのサイズである maxRect の境界内で行われます。 このロジックは MINIMUM フィールドも考慮し、トリミング四角形が完全に消失しないようにします。

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

メソッドの後半では、オプションの縦横比を調整します。

このクラスのすべての単位はピクセルであることに注意してください。

トリミング専用のキャンバス ビュー

先ほど説明した CroppingRectangle クラスは、SKCanvasView から派生した PhotoCropperCanvasView クラスで使用されます。 このクラスは、ビットマップとトリミング四角形を表示し、トリミング四角形を変更するためのタッチまたはマウス イベントを処理します。

PhotoCropperCanvasView コンストラクターにはビットマップが必要です。 縦横比はオプションです。 コンストラクターは、このビットマップと縦横比に基づいて CroppingRectangle 型のオブジェクトのインスタンスを作成し、フィールドとして保存します。

class PhotoCropperCanvasView : SKCanvasView
{
    ···
    SKBitmap bitmap;
    CroppingRectangle croppingRect;
    ···
    public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
    {
        this.bitmap = bitmap;

        SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
        croppingRect = new CroppingRectangle(bitmapRect, aspectRatio);
        ···
    }
    ···
}

このクラスは SKCanvasView から派生しているため、PaintSurface イベントのハンドラーをインストールする必要はありません。 代わりに、その OnPaintSurface メソッドをオーバーライドできます。 このメソッドはビットマップを表示し、フィールドとして保存されたいくつかの SKPaint オブジェクトを使用して、現在のトリミング四角形を描画します。

class PhotoCropperCanvasView : SKCanvasView
{
    const int CORNER = 50;      // pixel length of cropper corner
    ···
    SKBitmap bitmap;
    CroppingRectangle croppingRect;
    SKMatrix inverseBitmapMatrix;
    ···
    // Drawing objects
    SKPaint cornerStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.White,
        StrokeWidth = 10
    };

    SKPaint edgeStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.White,
        StrokeWidth = 2
    };
    ···
    protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
    {
        base.OnPaintSurface(args);

        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Gray);

        // Calculate rectangle for displaying bitmap
        float scale = Math.Min((float)info.Width / bitmap.Width, (float)info.Height / bitmap.Height);
        float x = (info.Width - scale * bitmap.Width) / 2;
        float y = (info.Height - scale * bitmap.Height) / 2;
        SKRect bitmapRect = new SKRect(x, y, x + scale * bitmap.Width, y + scale * bitmap.Height);
        canvas.DrawBitmap(bitmap, bitmapRect);

        // Calculate a matrix transform for displaying the cropping rectangle
        SKMatrix bitmapScaleMatrix = SKMatrix.MakeIdentity();
        bitmapScaleMatrix.SetScaleTranslate(scale, scale, x, y);

        // Display rectangle
        SKRect scaledCropRect = bitmapScaleMatrix.MapRect(croppingRect.Rect);
        canvas.DrawRect(scaledCropRect, edgeStroke);

        // Display heavier corners
        using (SKPath path = new SKPath())
        {
            path.MoveTo(scaledCropRect.Left, scaledCropRect.Top + CORNER);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Left + CORNER, scaledCropRect.Top);

            path.MoveTo(scaledCropRect.Right - CORNER, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Top);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Top + CORNER);

            path.MoveTo(scaledCropRect.Right, scaledCropRect.Bottom - CORNER);
            path.LineTo(scaledCropRect.Right, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Right - CORNER, scaledCropRect.Bottom);

            path.MoveTo(scaledCropRect.Left + CORNER, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom);
            path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom - CORNER);

            canvas.DrawPath(path, cornerStroke);
        }

        // Invert the transform for touch tracking
        bitmapScaleMatrix.TryInvert(out inverseBitmapMatrix);
    }
    ···
}

CroppingRectangle クラスのコードは、ビットマップのピクセル サイズに基づいてトリミング四角形を決定します。 ただし、PhotoCropperCanvasView クラスによるビットマップの表示は、表示領域のサイズに基づいて拡大縮小されます。 OnPaintSurface オーバーライドで計算された bitmapScaleMatrix は、ビットマップ ピクセルから、表示されるビットマップのサイズと位置にマップします。 次に、このマトリックスを使用してトリミング四角形を変換し、ビットマップを基準にして表示できるようにします。

OnPaintSurface オーバーライドの最後の行は bitmapScaleMatrix の逆数を取得し、inverseBitmapMatrix フィールドとして保存します。 これはタッチ処理に使用されます。

TouchEffect オブジェクトのインスタンスはフィールドとして作成され、コンストラクターは TouchAction イベントのハンドラーをアタッチしますが、TouchEffectSKCanvasView の派生物のEffects コレクションに追加する必要があり、これは OnParentSet オーバーライドで行われます。

class PhotoCropperCanvasView : SKCanvasView
{
    ···
    const int RADIUS = 100;     // pixel radius of touch hit-test
    ···
    CroppingRectangle croppingRect;
    SKMatrix inverseBitmapMatrix;

    // Touch tracking
    TouchEffect touchEffect = new TouchEffect();
    struct TouchPoint
    {
        public int CornerIndex { set; get; }
        public SKPoint Offset { set; get; }
    }

    Dictionary<long, TouchPoint> touchPoints = new Dictionary<long, TouchPoint>();
    ···
    public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
    {
        ···
        touchEffect.TouchAction += OnTouchEffectTouchAction;
    }
    ···
    protected override void OnParentSet()
    {
        base.OnParentSet();

        // Attach TouchEffect to parent view
        Parent.Effects.Add(touchEffect);
    }
    ···
    void OnTouchEffectTouchAction(object sender, TouchActionEventArgs args)
    {
        SKPoint pixelLocation = ConvertToPixel(args.Location);
        SKPoint bitmapLocation = inverseBitmapMatrix.MapPoint(pixelLocation);

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Convert radius to bitmap/cropping scale
                float radius = inverseBitmapMatrix.ScaleX * RADIUS;

                // Find corner that the finger is touching
                int cornerIndex = croppingRect.HitTest(bitmapLocation, radius);

                if (cornerIndex != -1 && !touchPoints.ContainsKey(args.Id))
                {
                    TouchPoint touchPoint = new TouchPoint
                    {
                        CornerIndex = cornerIndex,
                        Offset = bitmapLocation - croppingRect.Corners[cornerIndex]
                    };

                    touchPoints.Add(args.Id, touchPoint);
                }
                break;

            case TouchActionType.Moved:
                if (touchPoints.ContainsKey(args.Id))
                {
                    TouchPoint touchPoint = touchPoints[args.Id];
                    croppingRect.MoveCorner(touchPoint.CornerIndex,
                                            bitmapLocation - touchPoint.Offset);
                    InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchPoints.ContainsKey(args.Id))
                {
                    touchPoints.Remove(args.Id);
                }
                break;
        }
    }

    SKPoint ConvertToPixel(Xamarin.Forms.Point pt)
    {
        return new SKPoint((float)(CanvasSize.Width * pt.X / Width),
                           (float)(CanvasSize.Height * pt.Y / Height));
    }
}

TouchAction ハンドラーによって処理されるタッチ イベントの単位は、デバイスに依存しません。 これらは、最初にクラスの下部にある ConvertToPixel メソッドを使用してピクセルに変換され、次に inverseBitmapMatrix を使用して CroppingRectangle 単位に変換される必要があります。

Pressed イベントの場合、TouchAction ハンドラーは CroppingRectangleHitTest メソッドを呼び出します。 -1 以外のインデックスが返された場合、トリミング四角形のいずれかの隅が操作中の状態です。 そのインデックスと、隅からの実際のタッチ ポイントのオフセットは TouchPoint オブジェクトに保存され、touchPoints ディクショナリに追加されます。

Moved イベントの場合、CroppingRectangleMoveCorner メソッドを呼び出すと、縦横比が調整された状態で隅が移動します。

PhotoCropperCanvasView を使用するプログラムはいつでも CroppedBitmap プロパティにアクセスできます。 このプロパティは CroppingRectangleRect プロパティを使用して、トリミングされたサイズの新しいビットマップを作成します。 ターゲットとソースの四角形を指定した DrawBitmap のバージョンでは、元のビットマップからサブセットが抽出されます。

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

写真のトリミング キャンバス ビューのホスト

これらの 2 つのクラスでトリミング ロジックが処理される場合、SkiaSharpFormsDemos アプリケーションの [写真のトリミング] ページで行う作業はほとんどありません。 XAML ファイルは、PhotoCropperCanvasView[完了] ボタンをホストするための Grid のインスタンスを作成します。

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

SKBitmap 型のパラメーターが必要なため、XAML ファイルで PhotoCropperCanvasView のインスタンスを作成することはできません。

代わりに、PhotoCropperCanvasView のインスタンスは、リソース ビットマップの 1 つを使用する分離コード ファイルのコンストラクターで作成されます。

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

その後、ユーザーはトリミング四角形を操作できます。

Photo Cropper 1

適切なトリミング四角形が定義されたら、[完了] ボタンをクリックします。 Clicked ハンドラーは PhotoCropperCanvasViewCroppedBitmap プロパティからトリミングされたビットマップを取得し、ページのすべてのコンテンツを、このトリミングされたビットマップを表示する新しい SKCanvasView オブジェクトに置き換えます。

Photo Cropper 2

PhotoCropperCanvasView の 2 番目の引数を 1.78f (例) に設定してみてください。

photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);

トリミング四角形は、高精細テレビ特有の 16 対 9 の縦横比に制限されます。

ビットマップのタイルへの分割

有名な 14-15 パズルの Xamarin.Forms バージョンは、書籍『Xamarin.Forms を使用したモバイル アプリの作成』の第 22 章に登場し、XamagonXuzzle としてダウンロードできます。 ただし、パズルがユーザー自身の写真ライブラリの画像に基づいていると、より楽しくなります (多くの場合、より難しくなります)。

14-15 パズルのこのバージョンは SkiaSharpFormsDemos アプリケーションに含まれ、[写真パズル] というタイトルの一連のページで構成されています。

PhotoPuzzlePage1.xaml ファイルは 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>

分離コード ファイルでは、IPhotoLibrary 依存関係サービスを使用して、ユーザーが写真ライブラリから写真を選択できるようにする Clicked ハンドラーが実装されます。

public partial class PhotoPuzzlePage1 : ContentPage
{
    public PhotoPuzzlePage1 ()
    {
        InitializeComponent ();
    }

    async void OnPickButtonClicked(object sender, EventArgs args)
    {
        IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();
        using (Stream stream = await photoLibrary.PickPhotoAsync())
        {
            if (stream != null)
            {
                SKBitmap bitmap = SKBitmap.Decode(stream);

                await Navigation.PushAsync(new PhotoPuzzlePage2(bitmap));
            }
        }
    }
}

その後、メソッドは PhotoPuzzlePage2 に移動し、選択されたビットマップをコンストラクターに渡します。

ライブラリから選択された写真が写真ライブラリに表示されたとおりの向きではなく、回転していたり、上下が反転していたりする可能性があります (これは特に iOS デバイスの問題です)。そのため、PhotoPuzzlePage2 では、画像を必要な向きに回転できます。 XAML ファイルには、[90° 右向き] (時計回り)、[90° 左向き] (反時計回り)、[完了] というラベルの付いた 3 つのボタンが含まれています。

分離コード ファイルでは、記事「SkiaSharp ビットマップの作成と描画」で示されているビットマップ回転ロジックが実装されます。 ユーザーは、画像を何度でも 90 度時計回りまたは反時計回りに回転できます。

public partial class PhotoPuzzlePage2 : ContentPage
{
    SKBitmap bitmap;

    public PhotoPuzzlePage2 (SKBitmap bitmap)
    {
        this.bitmap = bitmap;

        InitializeComponent ();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
    }

    void OnRotateRightButtonClicked(object sender, EventArgs args)
    {
        SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear();
            canvas.Translate(bitmap.Height, 0);
            canvas.RotateDegrees(90);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = rotatedBitmap;
        canvasView.InvalidateSurface();
    }

    void OnRotateLeftButtonClicked(object sender, EventArgs args)
    {
        SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear();
            canvas.Translate(0, bitmap.Width);
            canvas.RotateDegrees(-90);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = rotatedBitmap;
        canvasView.InvalidateSurface();
    }

    async void OnDoneButtonClicked(object sender, EventArgs args)
    {
        await Navigation.PushAsync(new PhotoPuzzlePage3(bitmap));
    }
}

ユーザーが [完了] ボタンをクリックすると、Clicked ハンドラーは PhotoPuzzlePage3 に移動し、ページのコンストラクターで回転された最終的なビットマップを渡します。

PhotoPuzzlePage3 を使用すると、写真をトリミングできます。 プログラムには、4x4 のタイルのグリッドに分割するための正方形のビットマップが必要です。

PhotoPuzzlePage3.xaml ファイルには、LabelPhotoCropperCanvasView をホストする Grid、もう 1 つの [完了] ボタンが含まれています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SkiaSharpFormsDemos.Bitmaps.PhotoPuzzlePage3"
             Title="Photo Puzzle">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Label Text="Crop the photo to a square"
               Grid.Row="0"
               FontSize="Large"
               HorizontalTextAlignment="Center"
               Margin="5" />

        <Grid x:Name="canvasViewHost"
              Grid.Row="1"
              BackgroundColor="Gray"
              Padding="5" />

        <Button Text="Done"
                Grid.Row="2"
                HorizontalOptions="Center"
                Margin="5"
                Clicked="OnDoneButtonClicked" />
    </Grid>
</ContentPage>

分離コード ファイルは、コンストラクターに渡されたビットマップを使用して PhotoCropperCanvasView のインスタンスを作成します。 1 が 2 番目の引数として PhotoCropperCanvasView に渡されることに注意してください。 縦横比が 1 であるため、トリミング四角形は正方形になります。

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

[完了] ボタン ハンドラーは、トリミングされたビットマップの幅と高さを取得し (この 2 つの値は同じである必要があります)、それぞれの幅と高さが元のビットマップの 1/4 になっている 15 個の個別のビットマップに分割します (想定される 16 個のビットマップのうち、最後のビットマップは作成されません)。ソースとターゲットの四角形を指定した DrawBitmap メソッドにより、より大きなビットマップのサブセットに基づいたビットマップを作成できます。

Xamarin.Forms ビットマップへの変換

OnDoneButtonClicked メソッドでは、15 個のビットマップ用に作成された配列は ImageSource 型です。

ImageSource[] imgSources = new ImageSource[15];

ImageSource は、ビットマップをカプセル化する Xamarin.Forms 基本データ型です。 幸いにも、SkiaSharp では、SkiaSharp ビットマップから Xamarin.Forms ビットマップに変換できます。 SkiaSharp.Views.Forms アセンブリは、ImageSource から派生し、SkiaSharp SKBitmap オブジェクトに基づいて作成できる SKBitmapImageSource クラスを定義します。 SKBitmapImageSourceSKBitmapImageSourceSKBitmap の間の変換も定義します。これにより、SKBitmap オブジェクトが Xamarin.Forms ビットマップとして配列に保存されます。

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

このビットマップの配列は、コンストラクターとして PhotoPuzzlePage4 に渡されます。 そのページは完全に Xamarin.Forms であり、SkiaSharp は使用されません。 XamagonXuzzle に非常に似ているため、ここでは説明しませんが、選択した写真を 15 個の正方形のタイルに分割して表示します。

Photo Puzzle 1

[ランダム化] ボタンを押すと、すべてのタイルが混ざります。

Photo Puzzle 2

ここから、これらを正しい順序に戻すことができます。 空白の正方形と同じ行または列にあるタイルをタップして、空白の正方形に移動できます。