SkiaSharp ビットマップのトリミング
記事「SkiaSharp ビットマップの作成と描画」では、SKBitmap
オブジェクトを SKCanvas
コンストラクターに渡す方法について説明しました。 そのキャンバス上で呼び出された描画メソッドにより、グラフィックスがビットマップ上にレンダリングされます。 これらの描画メソッドには DrawBitmap
が含まれます。この手法では、1 つのビットマップの一部または全体を別のビットマップに転送でき、変換が適用されることもあります。
この手法を使用してビットマップをトリミングする場合は、ソースとターゲットの四角形を指定して DrawBitmap
メソッドを呼び出します。
canvas.DrawBitmap(bitmap, sourceRect, destRect);
ただし、トリミングを実装するアプリケーションには、ユーザーがトリミングする四角形を対話的に選択できるインターフェイスが用意されていることがよくあります。
この記事では、このインターフェイスを重点的に説明します。
トリミング四角形のカプセル化
トリミング ロジックの一部を 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
イベントのハンドラーをアタッチしますが、TouchEffect
を SKCanvasView
の派生物の親の 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
ハンドラーは CroppingRectangle
の HitTest
メソッドを呼び出します。 -1 以外のインデックスが返された場合、トリミング四角形のいずれかの隅が操作中の状態です。 そのインデックスと、隅からの実際のタッチ ポイントのオフセットは TouchPoint
オブジェクトに保存され、touchPoints
ディクショナリに追加されます。
Moved
イベントの場合、CroppingRectangle
の MoveCorner
メソッドを呼び出すと、縦横比が調整された状態で隅が移動します。
PhotoCropperCanvasView
を使用するプログラムはいつでも CroppedBitmap
プロパティにアクセスできます。 このプロパティは CroppingRectangle
の Rect
プロパティを使用して、トリミングされたサイズの新しいビットマップを作成します。 ターゲットとソースの四角形を指定した 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 つのクラスでトリミング ロジックが処理される場合、サンプル アプリケーションの [写真のトリミング] ページで行う作業はほとんどありません。 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);
}
}
その後、ユーザーはトリミング四角形を操作できます。
適切なトリミング四角形が定義されたら、[完了] ボタンをクリックします。 Clicked
ハンドラーは PhotoCropperCanvasView
の CroppedBitmap
プロパティからトリミングされたビットマップを取得し、ページのすべてのコンテンツを、このトリミングされたビットマップを表示する新しい SKCanvasView
オブジェクトに置き換えます。
PhotoCropperCanvasView
の 2 番目の引数を 1.78f (例) に設定してみてください。
photoCropper = new PhotoCropperCanvasView(bitmap, 1.78f);
トリミング四角形は、高精細テレビ特有の 16 対 9 の縦横比に制限されます。
ビットマップのタイルへの分割
有名な 14-15 パズルの Xamarin.Forms バージョンは、書籍『Xamarin.Forms を使用したモバイル アプリの作成』の第 22 章に登場し、XamagonXuzzle としてダウンロードできます。 ただし、パズルがユーザー自身の写真ライブラリの画像に基づいていると、より楽しくなります (多くの場合、より難しくなります)。
14-15 パズルのこのバージョンはサンプル アプリケーションに含まれ、[写真パズル] というタイトルの一連のページで構成されています。
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 ファイルには、Label
、PhotoCropperCanvasView
をホストする 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
クラスを定義します。 SKBitmapImageSource
は SKBitmapImageSource
と SKBitmap
の間の変換も定義します。これにより、SKBitmap
オブジェクトが Xamarin.Forms ビットマップとして配列に保存されます。
imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
このビットマップの配列は、コンストラクターとして PhotoPuzzlePage4
に渡されます。 そのページは完全に Xamarin.Forms であり、SkiaSharp は使用されません。 XamagonXuzzle に非常に似ているため、ここでは説明しませんが、選択した写真を 15 個の正方形のタイルに分割して表示します。
[ランダム化] ボタンを押すと、すべてのタイルが混ざります。
ここから、これらを正しい順序に戻すことができます。 空白の正方形と同じ行または列にあるタイルをタップして、空白の正方形に移動できます。