タッチ操作
マトリックス変換を使用してタッチによるドラッグ、ピンチ、回転を実装する
モバイル デバイスなどのマルチタッチ環境では、ユーザーはしばしば指を使って画面上のオブジェクトを操作します。 1 本指でのドラッグや 2 本指のピンチなどの一般的な動作で、オブジェクトの移動や拡大縮小、さらに回転も実行できます。 これらの動作は通常、変換マトリックスを使用して実装されます。この記事では、その方法について説明します。
ここに示すサンプルはすべて、「エフェクトからのイベントの呼び出し」記事に示されている Xamarin.Forms のタッチ トラッキング エフェクトを使用します。
ドラッグと変換
マトリックス変換の最も重要なアプリケーションの 1 つは、タッチ処理です。 1 つの SKMatrix
値が、一連のタッチ操作を統合できます。
1 本指でのドラッグの場合、SKMatrix
値が変換を実行します。 これを [Bitmap Dragging] ページに示します。 XAML ファイルが、SKCanvasView
を Xamarin.Forms の Grid
内でインスタンス化します。 TouchEffect
オブジェクトがその Grid
の Effects
コレクションに追加されました。
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.BitmapDraggingPage"
Title="Bitmap Dragging">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
理論上は、TouchEffect
オブジェクトを SKCanvasView
の Effects
コレクションに直接追加できますが、これはすべてのプラットフォームで機能するわけではありません。 この構成では、SKCanvasView
が Grid
と同じサイズであるため、Grid
にアタッチしても問題なく機能します。
分離コード ファイルが、ビットマップ リソースをそのコンストラクター内に読み込み、PaintSurface
ハンドラーに表示します。
public partial class BitmapDraggingPage : ContentPage
{
// Bitmap and matrix for display
SKBitmap bitmap;
SKMatrix matrix = SKMatrix.MakeIdentity();
···
public BitmapDraggingPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
canvas.SetMatrix(matrix);
canvas.DrawBitmap(bitmap, new SKPoint());
}
}
SKMatrix
値は、それ以上のコードなしで常にマトリックスを識別し、ビットマップの表示には影響を及ぼしません。 XAML ファイル内に設定される OnTouchEffectAction
ハンドラーの目標は、タッチ操作を反映するようにマトリックス値を変更することです。
OnTouchEffectAction
ハンドラーはまず、Xamarin.Forms の Point
値を SkiaSharp の SKPoint
値に変換します。 これは、SKCanvasView
の Width
プロパティおよび Height
プロパティ (デバイスに依存しない単位) と、CanvasSize
プロパティ (ピクセル単位) に基づく単純な拡大縮小の問題です。
public partial class BitmapDraggingPage : ContentPage
{
···
// Touch information
long touchId = -1;
SKPoint previousPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point))
{
touchId = args.Id;
previousPoint = point;
}
break;
case TouchActionType.Moved:
if (touchId == args.Id)
{
// Adjust the matrix for the new position
matrix.TransX += point.X - previousPoint.X;
matrix.TransY += point.Y - previousPoint.Y;
previousPoint = point;
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = -1;
break;
}
}
···
}
指が最初に画面に触れたときに、型 TouchActionType.Pressed
のイベントが発生します。 最初のタスクは、指がビットマップに触れているかどうかを判断することです。 このようなタスクはしばしば、"ヒット テスト" と呼ばれます。 このケースでは、ヒット テストは、ビットマップに対応する SKRect
値を作成し、MapRect
を使用してそれにマトリックス変換を適用してから、変換された四角形内にタッチ ポイントがあるかどうかを判断することで、実現できます。
あった場合は、touchId
フィールドがそのタッチ ID に設定され、指の位置が保存されます。
TouchActionType.Moved
イベントの場合、指の現在位置と新しい位置に基づいて、SKMatrix
値の変換係数が調整されます。 その新しい位置が次回まで保存され、SKCanvasView
が無効になります。
このプログラムを試しながら、ビットマップが表示されている領域に指が触れたときにのみビットマップをドラッグできることに注目してください。 この制約は、このプログラムにとってはあまり重要ではありませんが、複数のビットマップを操作するときには重要になります。
ピンチと拡大縮小
2 本の指でビットマップに触れるときは何が目的でしょうか。 2 本の指を並列に移動する場合は、おそらくビットマップを指に合わせて移動しようとしているでしょう。 2 本の指でピンチ操作または拡張操作を実行する場合は、ビットマップの回転 (次のセクションで説明します) か、拡大縮小を実行しようとしているでしょう。 ビットマップを拡大縮小するときは、2 本の指のビットマップに対する相対位置を同じままにし、それに応じてビットマップを拡大縮小させるのが最も合理的です。
2 本の指を同時に処理するのは複雑なようですが、TouchAction
ハンドラーは一時点では 1 本の指に関する情報のみを受け取ることを念頭に置いてください。 2 本の指がビットマップを操作していても、それぞれのイベントについては、1 本の指の位置が変更され、もう一方は変更されません。 以下の [Bitmap Scaling] ページのコードでは、位置が変更されていない指は "ピボット" ポイントと呼ばれます。変換がそのポイントに対して相対的に行われるためです。
このプログラムと前のプログラムの違いの 1 つは、複数のタッチ ID を保存する必要があることです。 この目的でディクショナリが使用されます。タッチ ID がディクショナリ キーであり、ディクショナリ値がその指の現在の位置です。
public partial class BitmapScalingPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point) && !touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Add(args.Id, point);
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger scale and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index of non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points involved in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Scaling factors are ratios of those
float scaleX = newVector.X / oldVector.X;
float scaleY = newVector.Y / oldVector.Y;
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
// If something bad hasn't happened, calculate a scale and translation matrix
SKMatrix scaleMatrix =
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
SKMatrix.PostConcat(ref matrix, scaleMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
···
}
Pressed
アクションの処理は、ID とタッチ ポイントがディクショナリに追加される点を除いて、前のプログラムとほぼ同じです。 Released
アクションと Cancelled
アクションは、ディクショナリ エントリを削除します。
ただし、Moved
アクションの処理はより複雑です。 関与する指が 1 本だけの場合、処理は前のプログラムとほぼ同じです。 2 本以上の指の場合、プログラムは、移動していない指に関する情報をディクショナリから取得することも必要になります。 これを行うには、ディクショナリ キーを配列にコピーしてから、最初のキーと、移動した指の ID とを比較します。 これで、移動していない指に対応するピボット ポイントをプログラムが取得できます。
次に、ピボット ポイントに対する新しい指の位置と、ピボットポイントに対する古い指の位置の 2 つのベクターをプログラムが計算します。 これらのベクターの比率が、拡大縮小係数です。 0 で除算する可能性があるため、無限値や NaN (数値ではない) 値がないか、これらを検査する必要があります。 すべて問題ない場合は、拡大縮小変換が、フィールドとして保存された SKMatrix
値に連結されます。
このページを試しながら、1 本または 2 本の指でのビットマップのドラッグや、2 本の指での拡大縮小を実行できることを確認します。 拡大縮小は "異方的" です。つまり、拡大縮小は水平方向と垂直方向で異なる可能性があります。 これにより、縦横比が歪みますが、ビットマップを反転して鏡像を作成することもできます。 ビットマップを 0 次元に縮小できることに気づかれるかもしれません。そうすると非表示になります。 運用環境のコードでは、こうならないように保護する必要があります。
2 本指での回転
[Bitmap Rotate] ページでは、2 本の指を使用して、回転または等方的な拡大縮小を行うことができます。 ビットマップは常に正しい縦横比を維持します。 回転と異方的な拡大縮小の両方に 2 本の指を使用しても、両方のタスクで指の動きが非常に似ているため、あまりうまく機能しません。
このプログラムの最初の大きな違いは、ヒット テストのロジックです。 前のプログラムでは、SKRect
の Contains
メソッドを使用して、タッチ ポイントがビットマップに対応する変換された四角形内にあるかどうかを判断しました。 しかし、ユーザーがビットマップを操作すると、ビットマップが回転するため、SKRect
が回転した四角形を適切に表すことができない可能性があります。 その場合、ヒット テストのロジックが、やや複雑な分析ジオメトリを実装する必要があることを懸念されるかもしれません。
ただし、抜け道があります。あるポイントが変換された四角形の境界内にあるかどうかを判断することは、逆変換されたポイントが変換されていない四角形の境界内にあるかどうかを判断するのと同じです。 これははるかに簡単な計算であり、ロジックは引き続き便利な Contains
メソッドを使用できます。
public partial class BitmapRotationPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (!touchDictionary.ContainsKey(args.Id))
{
// Invert the matrix
if (matrix.TryInvert(out SKMatrix inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(point);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
if (rect.Contains(transformedPoint))
{
touchDictionary.Add(args.Id, point);
}
}
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger rotate, scale, and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Isotropic scaling!
float scale = Magnitude(newVector) / Magnitude(oldVector);
if (!float.IsNaN(scale) && !float.IsInfinity(scale))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));
SKMatrix.PostConcat(ref matrix, touchMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
···
}
Moved
イベントのロジックは、前のプログラムと同様に開始されます。 oldVector
と newVector
という名前の 2 つのベクターが、移動している指の前のポイントおよび現在のポイントと、移動していない指のピボット ポイントに基づいて計算されます。 しかし、次にこれらのベクターの角度が判断されます。違いは回転角度です。
拡大縮小も発生する可能性があるため、古いベクターが回転角度に基づいて回転されます。 これで、2 つのベクターの相対的な大きさが拡大縮小係数になりました。 拡大縮小が等方的になるように、水平方向と垂直方向の拡大縮小に同じ scale
値が使用されていることに注意してください。 matrix
フィールドは、回転マトリックスと拡大縮小マトリックスの両方によって調整されます。
アプリケーションで 1 つのビットマップ (または他のオブジェクト) のタッチ処理を実装する必要がある場合は、これら 3 つのサンプルのコードを独自のアプリケーションに合わせて調整できます。 ただし、複数のビットマップのタッチ処理を実装する必要がある場合は、これらのタッチ操作を他のクラスにカプセル化することが考えられます。
タッチ操作のカプセル化
[Touch Manipulation] ページは、1 つのビットマップのタッチ操作を示しますが、上記のロジックの多くをカプセル化する他のいくつかのファイルを使用します。 これらのファイルの 1 番めは TouchManipulationMode
列挙です。これは、次に示すコードによって実装されるさまざまな種類のタッチ操作を示します。
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
は、変換とともに実装される 1 本指のドラッグです。 それ以降のすべてのオプションにもパンが含まれますが、2 本の指が使用されます。IsotropicScale
は、オブジェクトが水平方向と垂直方向に等しく拡大縮小されるピンチ操作です。 AnisotropicScale
を使用すると、不均衡な拡大縮小が可能になります。
ScaleRotate
オプションは、2 本指での拡大縮小と回転用です。 拡大縮小は等方的です。 前述のように、異方的な拡大縮小を行う 2 本指の回転を実装することは、指の動きが本質的に同じであるために難しくなります。
ScaleDualRotate
オプションは、1 本指での回転を追加します。 1 本の指がオブジェクトをドラッグすると、ドラッグされたオブジェクトはまずその中心の周りを回転して、オブジェクトの中心がドラッグ ベクターに合うように移動します。
TouchManipulationPage.xaml ファイルには、TouchManipulationMode
列挙型のメンバーの Picker
が含まれています。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
Title="Touch Manipulation">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker Title="Touch Mode"
Grid.Row="0"
SelectedIndexChanged="OnTouchModePickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:TouchManipulationMode}">
<x:Static Member="local:TouchManipulationMode.None" />
<x:Static Member="local:TouchManipulationMode.PanOnly" />
<x:Static Member="local:TouchManipulationMode.IsotropicScale" />
<x:Static Member="local:TouchManipulationMode.AnisotropicScale" />
<x:Static Member="local:TouchManipulationMode.ScaleRotate" />
<x:Static Member="local:TouchManipulationMode.ScaleDualRotate" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
4
</Picker.SelectedIndex>
</Picker>
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</Grid>
</ContentPage>
終わり付近には、SKCanvasView
と、それを含む単一セルの Grid
にアタッチされている TouchEffect
があります。
TouchManipulationPage.xaml.cs 分離コード ファイルには bitmap
フィールドがありますが、型が SKBitmap
ではありません。 その型は TouchManipulationBitmap
(すぐ下に示すクラス) です。
public partial class TouchManipulationPage : ContentPage
{
TouchManipulationBitmap bitmap;
...
public TouchManipulationPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
this.bitmap = new TouchManipulationBitmap(bitmap);
this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
}
}
...
}
コンストラクターが TouchManipulationBitmap
オブジェクトをインスタンス化し、埋め込みリソースから取得した SKBitmap
をコンストラクターに渡します。 コンストラクターは、TouchManipulationBitmap
オブジェクトの TouchManager
プロパティの Mode
プロパティを TouchManipulationMode
列挙型のメンバーに設定して終了します。
Picker
の SelectedIndexChanged
ハンドラーも、この Mode
プロパティを設定します。
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
XAML ファイルでインスタンス化された TouchEffect
の TouchAction
ハンドラーが、TouchManipulationBitmap
の HitTest
と ProcessTouchEvent
という名前の 2 つのメソッドを呼び出します。
public partial class TouchManipulationPage : ContentPage
{
...
List<long> touchIds = new List<long>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (bitmap.HitTest(point))
{
touchIds.Add(args.Id);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
break;
}
break;
case TouchActionType.Moved:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
touchIds.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
HitTest
メソッドが true
を返した (つまり、画面の、ビットマップが占有している領域内に指が触れた) 場合、タッチ ID が TouchIds
コレクションに追加されます。 この ID は、指が画面から離れるまで、その指の一連のタッチ イベントを表します。 複数の指がビットマップにタッチした場合、touchIds
コレクションには各指のタッチ ID が含められます。
TouchAction
ハンドラーも、TouchManipulationBitmap
の ProcessTouchEvent
クラスを呼び出します。 ここで、実際のタッチ処理の一部 (すべてではない) が発生します。
TouchManipulationBitmap
クラスは、ビットマップをレンダリングしてタッチ イベントを処理するコードを含む SKBitmap
のラッパー クラスです。 これは、TouchManipulationManager
クラス (すぐ下に示します) 内のより汎用的なコードと連携して動作します。
TouchManipulationBitmap
コンストラクターが、SKBitmap
を保存し、型 TouchManipulationManager
の TouchManager
プロパティと型 SKMatrix
の Matrix
プロパティの 2 つのプロパティをインスタンス化します。
class TouchManipulationBitmap
{
SKBitmap bitmap;
...
public TouchManipulationBitmap(SKBitmap bitmap)
{
this.bitmap = bitmap;
Matrix = SKMatrix.MakeIdentity();
TouchManager = new TouchManipulationManager
{
Mode = TouchManipulationMode.ScaleRotate
};
}
public TouchManipulationManager TouchManager { set; get; }
public SKMatrix Matrix { set; get; }
...
}
この Matrix
プロパティは、すべてのタッチ アクティビティの結果として累積された変換です。 ご覧のように、各タッチ イベントはマトリックスに解決されてから、Matrix
プロパティが格納した SKMatrix
値と連結されます。
TouchManipulationBitmap
オブジェクトが、その Paint
メソッド内に自身を描画します。 引数は、SKCanvas
オブジェクトです。 この SKCanvas
には既に変換が適用されている可能性があるため、Paint
メソッドはビットマップに関連付けられている Matrix
プロパティを既存の変換に連結し、それが完了したらキャンバスを復元します。
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
HitTest
メソッドは、ユーザーが画面でビットマップの境界内のポイントにタッチした場合、true
を返します。 これは、前に [Bitmap Rotation] ページで示したロジックを使用します。
class TouchManipulationBitmap
{
...
public bool HitTest(SKPoint location)
{
// Invert the matrix
SKMatrix inverseMatrix;
if (Matrix.TryInvert(out inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(location);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
return rect.Contains(transformedPoint);
}
return false;
}
...
}
TouchManipulationBitmap
内の 2 番めのパブリック メソッドは ProcessTouchEvent
です。 このメソッドが呼び出されるときには、タッチ イベントがこの特定のビットマップに属することが既に確定しています。 このメソッドは、TouchManipulationInfo
オブジェクトのディクショナリを維持します。これは、単純に各指の前のポイントと新しいポイントです。
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
ディクショナリと ProcessTouchEvent
メソッド自体を次に示します。
class TouchManipulationBitmap
{
...
Dictionary<long, TouchManipulationInfo> touchDictionary =
new Dictionary<long, TouchManipulationInfo>();
...
public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
{
switch (type)
{
case TouchActionType.Pressed:
touchDictionary.Add(id, new TouchManipulationInfo
{
PreviousPoint = location,
NewPoint = location
});
break;
case TouchActionType.Moved:
TouchManipulationInfo info = touchDictionary[id];
info.NewPoint = location;
Manipulate();
info.PreviousPoint = info.NewPoint;
break;
case TouchActionType.Released:
touchDictionary[id].NewPoint = location;
Manipulate();
touchDictionary.Remove(id);
break;
case TouchActionType.Cancelled:
touchDictionary.Remove(id);
break;
}
}
...
}
Moved
イベントと Released
イベントで、このメソッドが Manipulate
を呼び出します。 これらの時点で、touchDictionary
には 1 つ以上の TouchManipulationInfo
オブジェクトが含まれています。 touchDictionary
に含まれている項目が 1 つの場合は、PreviousPoint
値と NewPoint
値が等しくなく、おそらく 1 本の指の動きを表しています。 複数の指がビットマップに触れている場合、ディクショナリには複数の項目が含まれていますが、PreviousPoint
値と NewPoint
値が異なる項目は、これらのうち 1 つのみです。 残りはすべて等しい PreviousPoint
値と NewPoint
値を持ちます。
これは重要です。Manipulate
メソッドは、1 本の指のみの動きを処理していると想定する可能性があります。 この呼び出しの時点で、他の指は動いていません。もし実際には動いている (あり得ることですが) 場合、それらの動きはその後の Manipulate
への呼び出しで処理されます。
Manipulate
メソッドは、便宜上、まずディクショナリを配列にコピーします。 これは、最初の 2 つのエントリ以外のエントリを無視します。 2 本以上の指でビットマップを操作しようとすると、他の指が無視されます。 Manipulate
は TouchManipulationBitmap
の最後のメンバーです。
class TouchManipulationBitmap
{
...
void Manipulate()
{
TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
touchDictionary.Values.CopyTo(infos, 0);
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
if (infos.Length == 1)
{
SKPoint prevPoint = infos[0].PreviousPoint;
SKPoint newPoint = infos[0].NewPoint;
SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);
touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
}
else if (infos.Length >= 2)
{
int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
SKPoint pivotPoint = infos[pivotIndex].NewPoint;
SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;
touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
}
SKMatrix matrix = Matrix;
SKMatrix.PostConcat(ref matrix, touchMatrix);
Matrix = matrix;
}
}
1 本の指でビットマップを操作している場合、Manipulate
は TouchManipulationManager
オブジェクトの OneFingerManipulate
メソッドを呼び出します。 2 本の指の場合、TwoFingerManipulate
を呼び出します。 これらのメソッドの prevPoint
引数と newPoint
引数は同じであり、移動中の指を表します。 しかし、この 2 つの呼び出しで pivotPoint
引数は異なります。
1 本指で操作する場合、pivotPoint
はビットマップの中心になります。 これは、1 本指での回転を可能にするためです。 2 本指で操作する場合、イベントは 1 本の指の動きのみを示し、pivotPoint
は動いていない指になります。
どちらの場合も TouchManipulationManager
は、SKMatrix
値を返します。これをメソッドが現在の Matrix
プロパティと連結して、TouchManipulationPage
がビットマップのレンダリングに使用します。
TouchManipulationManager
は一般化されていて、TouchManipulationMode
以外のファイルを使用しません。 独自のアプリケーションで変更を加えることなく、このクラスを使用できる場合があります。 TouchManipulationMode
型の単一プロパティが定義されます。
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
ただし、AnisotropicScale
オプションは避けることをお勧めします。 このオプションを使用すると、ビットマップを操作して拡大縮小係数の 1 つをゼロにするのはいとも簡単です。 これにより、ビットマップが見えなくなり、元に戻ることはありません。 異方的な拡大縮小が本当に必要な場合は、望ましくない結果を避けるためにロジックを強化する必要があります。
TouchManipulationManager
はベクターを使用しますが、SkiaSharp には SKVector
構造がないため、代わりに SKPoint
が使用されます。 SKPoint
は減算演算子をサポートし、結果をベクターとして扱うことができます。 追加する必要があるベクター固有のロジックは、Magnitude
計算だけです。
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
回転が選択されるたびに、1 本指と 2 本指の両方の操作メソッドがまず回転を処理します。 回転が検出されると、回転コンポーネントは実質的に削除されます。 残るのは、パンと拡大縮小であると解釈されます。
OneFingerManipulate
メソッドを次に示します。 1 本指の回転が有効になっていない場合、ロジックは単純です。単純に前のポイントと新しいポイントを使用して、変換に正確に対応する delta
という名前のベクターを構築します。 1 本指での回転を有効にすると、このメソッドはピボット ポイント (ビットマップの中心) から前のポイントと新しいポイントまでの角度を使用して回転マトリックスを構築します。
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
if (Mode == TouchManipulationMode.None)
{
return SKMatrix.MakeIdentity();
}
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint delta = newPoint - prevPoint;
if (Mode == TouchManipulationMode.ScaleDualRotate) // One-finger rotation
{
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Avoid rotation if fingers are too close to center
if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
{
float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - prevAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Recalculate delta
delta = newVector - oldVector;
}
}
// Multiply the rotation matrix by a translation matrix
SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));
return touchMatrix;
}
...
}
TwoFingerManipulate
メソッドでは、ピボット ポイントは、この特定のタッチ イベントで動いていない指の位置です。 この回転は 1 本指の回転に非常によく似ていて、oldVector
という名前のベクター (前のポイントに基づくもの) が回転に合わせて調整されます。 残りの移動は拡大縮小であると解釈されます。
class TouchManipulationManager
{
...
public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
if (Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
}
float scaleX = 1;
float scaleY = 1;
if (Mode == TouchManipulationMode.AnisotropicScale)
{
scaleX = newVector.X / oldVector.X;
scaleY = newVector.Y / oldVector.Y;
}
else if (Mode == TouchManipulationMode.IsotropicScale ||
Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
}
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
}
return touchMatrix;
}
...
}
このメソッドには明示的な変換がないことがわかります。 ただし、MakeRotation
メソッドと MakeScale
メソッドの両方ともピボット ポイントに基づいているため、これには暗黙的な変換が含まれます。 ビットマップで 2 本の指を使用し、同じ方向にドラッグすると、TouchManipulation
は、2 本の指で交互に発生する一連のタッチ イベントを取得します。 各指が他の指に対して相対的に移動すると、拡大縮小または回転が発生しますが、もう一方の指の動きによって否定され、変換が発生する結果になります。
[Touch Manipulation] ページで唯一残っているパーツは、TouchManipulationPage
分離コード ファイル内の PaintSurface
ハンドラーです。 これは TouchManipulationBitmap
の Paint
メソッドを呼び出します。これは、累積タッチ アクティビティを表すマトリックスを適用します。
public partial class TouchManipulationPage : ContentPage
{
...
MatrixDisplay matrixDisplay = new MatrixDisplay();
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
bitmap.Paint(canvas);
// Display the matrix in the lower-right corner
SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);
matrixDisplay.Paint(canvas, bitmap.Matrix,
new SKPoint(info.Width - matrixSize.Width,
info.Height - matrixSize.Height));
}
}
PaintSurface
ハンドラーは、累積されたタッチ マトリックスを示す MatrixDisplay
オブジェクトを表示して終了します。
複数のビットマップの操作
TouchManipulationBitmap
や TouchManipulationManager
などのクラスでタッチ処理コードを分離する利点の 1 つは、プログラムでこれらのクラスを再利用できることであり、これを使用すると、ユーザーが複数のビットマップを操作できるようになります。
[Bitmap Scatter View] ページは、これを行う方法を示しています。 BitmapScatterPage
クラスは、型 TouchManipulationBitmap
のフィールドを定義するのではなく、ビットマップ オブジェクトの List
を定義します。
public partial class BitmapScatterViewPage : ContentPage
{
List<TouchManipulationBitmap> bitmapCollection =
new List<TouchManipulationBitmap>();
...
public BitmapScatterViewPage()
{
InitializeComponent();
// Load in all the available bitmaps
Assembly assembly = GetType().GetTypeInfo().Assembly;
string[] resourceIDs = assembly.GetManifestResourceNames();
SKPoint position = new SKPoint();
foreach (string resourceID in resourceIDs)
{
if (resourceID.EndsWith(".png") ||
resourceID.EndsWith(".jpg"))
{
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
{
Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
});
position.X += 100;
position.Y += 100;
}
}
}
}
...
}
コンストラクターは、埋め込みリソースとして使用できるすべてのビットマップを読み込み、それらを bitmapCollection
に追加します。 Matrix
プロパティは TouchManipulationBitmap
オブジェクトごとに初期化されるため、各ビットマップの左上隅が 100 ピクセル分オフセットされることに注意してください。
BitmapScatterView
ページは、複数のビットマップのタッチ イベントも処理する必要があります。 このプログラムは、現在操作されている TouchManipulationBitmap
オブジェクトのタッチ ID の List
を定義するのではなく、ディクショナリを必要とします。
public partial class BitmapScatterViewPage : ContentPage
{
...
Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
new Dictionary<long, TouchManipulationBitmap>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
for (int i = bitmapCollection.Count - 1; i >= 0; i--)
{
TouchManipulationBitmap bitmap = bitmapCollection[i];
if (bitmap.HitTest(point))
{
// Move bitmap to end of collection
bitmapCollection.Remove(bitmap);
bitmapCollection.Add(bitmap);
// Do the touch processing
bitmapDictionary.Add(args.Id, bitmap);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
break;
}
}
break;
case TouchActionType.Moved:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
bitmapDictionary.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
Pressed
ロジックが bitmapCollection
を逆向きにループ処理するしくみに注目してください。 ビットマップはしばしば、互いに重なり合います。 コレクションの後方にあるビットマップは、コレクションの前方にあるビットマップの上に視覚的に配置されます。 画面上で押す指の下に複数のビットマップがある場合、最上位のビットマップが、その指によって操作されるビットマップであるはずです。
また、Pressed
ロジックがそのビットマップをコレクションの末尾に移動して、他のビットマップの積み重ねの頂上に視覚的に移動します。
Moved
イベントと Released
イベントでは、TouchAction
ハンドラーが前のプログラムと同様に TouchManipulationBitmap
の ProcessingTouchEvent
メソッドを呼び出します。
最後に、PaintSurface
ハンドラーが各 TouchManipulationBitmap
オブジェクトの Paint
メソッドを呼び出します。
public partial class BitmapScatterViewPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (TouchManipulationBitmap bitmap in bitmapCollection)
{
bitmap.Paint(canvas);
}
}
}
コードはコレクションをループ処理し、コレクションの先頭から末尾までのビットマップの積み重ねを表示します。
1 本指での拡大縮小
拡大縮小操作には通常、2 本の指を使用したピンチ ジェスチャが必要です。 ただし、指でビットマップの角を移動することで、1 本指での拡大縮小を実装できます。
これを、[Single Finger Corner Scale] ページに示します。 このサンプルは、TouchManipulationManager
クラスに実装されている拡大縮小とは多少異なる種類の拡大縮小を使用するため、そのクラスや TouchManipulationBitmap
クラスは使用しません。 代わりに、すべてのタッチ ロジックが分離コード ファイルにあります。 これは、一度に 1 本の指のみを追跡し、画面に触れている可能性があるそれ以外の指を単に無視するため、通常よりもやや単純なロジックです。
SingleFingerCornerScale.xaml ページは、SKCanvasView
クラスをインスタンス化し、タッチ イベントを追跡するための TouchEffect
オブジェクトを作成します。
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
Title="Single Finger Corner Scale">
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
SingleFingerCornerScalePage.xaml.cs ファイルは、Media ディレクトリからビットマップ リソースを読み込み、フィールドとして定義された SKMatrix
オブジェクトを使用して表示します。
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
···
public SingleFingerCornerScalePage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.SetMatrix(currentMatrix);
canvas.DrawBitmap(bitmap, 0, 0);
}
···
}
この SKMatrix
オブジェクトは、以下に示すタッチ ロジックによって変更されます。
分離コード ファイルの残りの部分は TouchEffect
イベント ハンドラーです。 まず、指の現在の位置を SKPoint
値に変換します。 アクションの種類が Pressed
の場合、ハンドラーが、他の指が画面に触れていないことと、触れた指がビットマップの境界内にあることを検査します。
コードの重要な部分は、Math.Pow
メソッドへの 2 つの呼び出しを含む if
ステートメントです。 この数式は、指の位置がビットマップ内を埋める楕円の外側にあるかどうかを調べます。 ある場合、拡大縮小操作です。 指がビットマップの角の 1 つに近く、ピボット ポイントが反対の角であると判断されます。 指がこの楕円内にある場合は、通常のパン操作です。
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
// Information for translating and scaling
long? touchId = null;
SKPoint pressedLocation;
SKMatrix pressedMatrix;
// Information for scaling
bool isScaling;
SKPoint pivotPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Track only one finger
if (touchId.HasValue)
return;
// Check if the finger is within the boundaries of the bitmap
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = currentMatrix.MapRect(rect);
if (!rect.Contains(point))
return;
// First assume there will be no scaling
isScaling = false;
// If touch is outside interior ellipse, make this a scaling operation
if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
{
isScaling = true;
float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
pivotPoint = new SKPoint(xPivot, yPivot);
}
// Common for either pan or scale
touchId = args.Id;
pressedLocation = point;
pressedMatrix = currentMatrix;
break;
case TouchActionType.Moved:
if (!touchId.HasValue || args.Id != touchId.Value)
return;
SKMatrix matrix = SKMatrix.MakeIdentity();
// Translating
if (!isScaling)
{
SKPoint delta = point - pressedLocation;
matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
}
// Scaling
else
{
float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
}
// Concatenate the matrices
SKMatrix.PreConcat(ref matrix, pressedMatrix);
currentMatrix = matrix;
canvasView.InvalidateSurface();
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = null;
break;
}
}
}
アクションの種類 Moved
は、指が画面を押した時点からこの時点までのタッチ アクティビティに対応するマトリックスを計算します。 そのマトリックスを、指がビットマップを最初に押した時点で有効だったマトリックスと連結します。 拡大縮小操作は常に、指が触れた角とは反対側の角に対して相対的です。
小さいビットマップまたは長いビットマップの場合、内部の楕円がビットマップの大部分を占有し、ビットマップを拡大縮小するために残される角の領域が小さくなる場合があります。 少し異なるアプローチを使用することが考えられます。その場合は、if
ブロック全体を、isScaling
を true
に設定するこのコードに置き換えることができます。
float halfHeight = rect.Height / 2;
float halfWidth = rect.Width / 2;
// Top half of bitmap
if (point.Y < rect.MidY)
{
float yRelative = (point.Y - rect.Top) / halfHeight;
// Upper-left corner
if (point.X < rect.MidX - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Bottom);
}
// Upper-right corner
else if (point.X > rect.MidX + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Bottom);
}
}
// Bottom half of bitmap
else
{
float yRelative = (point.Y - rect.MidY) / halfHeight;
// Lower-left corner
if (point.X < rect.Left + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Top);
}
// Lower-right corner
else if (point.X > rect.Right - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Top);
}
}
このコードは、ビットマップの領域を内側のひし形と、角の 4 つの三角形に実質的に分割します。 これにより、角の領域がより大きくなるため、それに触れてビットマップを拡大縮小できます。