エフェクトからのイベントの呼び出し
エフェクトではイベントを定義して呼び出すことができ、基礎ネイティブ ビューの変化を信号で送ります。 この記事では、低レベルのマルチタッチ フィンガー トラッキングを実装する方法とタッチ操作を信号で送るイベントを生成する方法を紹介します。
この記事で説明するエフェクトでは、低レベルのタッチ イベントにアクセスできます。 このような低レベルのイベントは既存の GestureRecognizer
クラス経由では利用できませんが、一部のアプリケーションにとって不可欠です。 たとえば、フィンガーペイント アプリケーションの場合、画面上を動く一本一本の指を追跡する必要があります。 音楽用キーボードでは、個々の鍵盤を押す/離す動きやグリッサンドで鍵盤上をすべる指を検出する必要があります。
エフェクトはあらゆる Xamarin.Forms 要素にアタッチできるため、マルチタッチ フィンガー トラッキングに最適です。
プラットフォーム タッチ イベント
iOS、Android、Universal Windows Platform にはすべて、タッチ操作の検出をアプリケーションに許可する低レベル API が含まれています。 このようなプラットフォームではすべて、基本的な 3 種類のタッチ イベントが区別されます。
- 押す。指が画面に触れたとき
- 移動する。画面に触れている指が移動するとき
- 離す。指を画面から離したとき
マルチタッチ環境では、複数の指で同時に画面に触れることができます。 複数の指を区別するためにアプリケーションで利用される ID 番号が各種プラットフォームに含まれています。
iOS の場合、UIView
クラスによって、これら 3 つの基本イベントに相当する 3 つのオーバーライド可能メソッド、TouchesBegan
、TouchesMoved
、TouchesEnded
が定義されます。 「Multi-Touch Finger Tracking」 (マルチタッチ フィンガー トラッキング) という記事に、これらのメソッドの使用方法が記載されています。 しかしながら、iOS プログラムの場合、これらのメソッドを使用するには UIView
から派生するクラスをオーバーライドする必要があります。 iOS UIGestureRecognizer
では、これらの同じ 3 つのメソッドも定義されます。UIGestureRecognizer
から派生するクラスのインスタンスをあらゆる UIView
オブジェクトにアタッチできます。
Android の場合、View
クラスによって、すべてのタッチ操作を処理する OnTouchEvent
という名称のオーバーライド可能メソッドが定義されます。 タッチ操作の種類は、「Multi-Touch Finger Tracking」 (マルチタッチ フィンガー トラッキング) という記事で説明されている列挙メンバー Down
、PointerDown
、Move
、Up
、PointerUp
によって定義されます。 Android View
により Touch
という名前のイベントも定義されます。このイベントによって、イベント ハンドラーをあらゆる View
オブジェクトにアタッチできます。
ユニバーサル Windows プラットフォーム (UWP) では、UIElement
クラスによって PointerPressed
、PointerMoved
、PointerReleased
という名前のイベントが定義されます。 これらのイベントに関する説明は、MSDN のポインター入力の処理に関する記事と UIElement
クラスに関する API 文書にあります。
ユニバーサル Windows プラットフォームの Pointer
API はマウス、タッチ、ペンによる入力の統一を意図するものです。 そのため、マウス ボタンが押されていないときでも、マウスが要素を横切ると、PointerMoved
イベントが呼び出されます。 このようなイベントにともなう PointerRoutedEventArgs
オブジェクトには Pointer
という名前のプロパティがあります。このプロパティには、マウス ボタンが押されたかどうかや指が画面と接触しているかどうかを示す IsInContact
プロパティがあります。
さらに、UWP によって PointerEntered
と PointerExited
という 2 つのイベントがさらに定義されます。 この 2 つのイベントは、マウスまたは指が要素間を移動したタイミングを示します。 たとえば、A と B という名前が付けられた 2 つの隣接する要素があるとします。いずれの要素にも、ポインター イベントのハンドラーがインストールされています。 指が A を押すと、PointerPressed
イベントが呼び出されます。 指が動くと、A によって PointerMoved
イベントが呼び出されます。 指が A から B に移動すると、A によって PointerExited
イベントが呼び出され、B によって PointerEntered
イベントが呼び出されます。 指を離すと、B によって PointerReleased
イベントが呼び出されます。
iOS プラットフォームと Android プラットフォームは UWP とは異なります。指がビューに触れたとき、TouchesBegan
または OnTouchEvent
の呼び出しを最初に取得するビューは、指が別のビューに移動した場合でも、すべてのタッチ操作を引き続き取得します。 アプリケーションでポインターがキャプチャされた場合、UWP は同様に動作できます。PointerEntered
イベント ハンドラーで、要素によって CapturePointer
が呼び出され、その指からのすべてのタッチ操作が取得されます。
UWP の手法は、音楽用キーボードなど、一部のアプリケーションで非常に便利になります。 各キーはそのキーのタッチ イベントを処理し、PointerEntered
イベントと PointerExited
イベントによってキー間の指のスライド移動を検出できます。
そのような理由から、この記事で説明するタッチトラッキング エフェクトでは UWP の手法を実行します。
タッチトラッキング エフェクト API
サンプルには、低レベルのタッチトラッキングを実行するクラス (と列挙) が含まれています。 これらの型は名前空間 TouchTracking
に属し、Touch
という単語から始まります。 TouchTrackingEffectDemos .NET Standard ライブラリ プロジェクトには、タッチ イベント型の TouchActionType
列挙が含まれています。
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
プラットフォームにはすべて、タッチ イベントがキャンセルされたことを示すイベントが含まれています。
.NET Standard ライブラリの TouchEffect
クラスは RoutingEffect
から派生し、このクラスによって TouchAction
という名前のイベントと TouchAction
イベントを呼び出す OnTouchAction
という名前のメソッドが定義されます。
public class TouchEffect : RoutingEffect
{
public event TouchActionEventHandler TouchAction;
public TouchEffect() : base("XamarinDocs.TouchEffect")
{
}
public bool Capture { set; get; }
public void OnTouchAction(Element element, TouchActionEventArgs args)
{
TouchAction?.Invoke(element, args);
}
}
Capture
プロパティにも注目してください。 タッチ イベントをキャプチャするには、Pressed
イベントの前にアプリケーションでこのプロパティを true
に設定する必要があります。 そうしない場合、ユニバーサル Windows プラットフォームの場合のようにタッチ イベントが動作します。
.NET Standard ライブラリの TouchActionEventArgs
クラスには、各イベントにともなう情報がすべて含まれます。
public class TouchActionEventArgs : EventArgs
{
public TouchActionEventArgs(long id, TouchActionType type, Point location, bool isInContact)
{
Id = id;
Type = type;
Location = location;
IsInContact = isInContact;
}
public long Id { private set; get; }
public TouchActionType Type { private set; get; }
public Point Location { private set; get; }
public bool IsInContact { private set; get; }
}
アプリケーションでは、Id
プロパティを利用して一本一本の指を追跡できます。 IsInContact
プロパティに注目してください。 このプロパティは常に Pressed
イベントに対して true
、Released
イベントに対して false
となります。 iOS と Android でも、Moved
イベントに対して常に true
となります。 ユニバーサル Windows プラットフォームでは、プログラムがデスクトップで実行されているとき、ボタンを押さずにマウス ポインターが動いたとき、IsInContact
プロパティは Moved
イベントに対して false
にであることがあります。
ソリューションの .NET Standard ライブラリ プロジェクトにファイルを含め、任意の Xamarin.Forms 要素の Effects
コレクションにインスタンスを追加することで、自分のアプリケーションで TouchEffect
クラスを使用できます。 タッチ イベントを取得するには、TouchAction
イベントにハンドラーをアタッチします。
自分のアプリケーションで TouchEffect
を使用するには、TouchTrackingEffectDemos ソリューションに含まれるプラットフォーム実装も必要になります。
タッチトラッキング エフェクト実装
以下は TouchEffect
の iOS、Android、UWP 実装の説明となります。最も単純な実装 (UWP) で始まり、構造的に他より複雑な iOS 実装で終わっています。
UWP 実装
TouchEffect
の UWP 実装が最も単純です。 通常どおり、このクラスは PlatformEffect
から派生し、2 つのアセンブリ属性を含みます。
[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]
namespace TouchTracking.UWP
{
public class TouchEffect : PlatformEffect
{
...
}
}
OnAttached
オーバーライドによって一部の情報がフィールドとして保存され、ハンドラーがすべてのポインター イベントにアタッチされます。
public class TouchEffect : PlatformEffect
{
FrameworkElement frameworkElement;
TouchTracking.TouchEffect effect;
Action<Element, TouchActionEventArgs> onTouchAction;
protected override void OnAttached()
{
// Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
frameworkElement = Control == null ? Container : Control;
// Get access to the TouchEffect class in the .NET Standard library
effect = (TouchTracking.TouchEffect)Element.Effects.
FirstOrDefault(e => e is TouchTracking.TouchEffect);
if (effect != null && frameworkElement != null)
{
// Save the method to call on touch events
onTouchAction = effect.OnTouchAction;
// Set event handlers on FrameworkElement
frameworkElement.PointerEntered += OnPointerEntered;
frameworkElement.PointerPressed += OnPointerPressed;
frameworkElement.PointerMoved += OnPointerMoved;
frameworkElement.PointerReleased += OnPointerReleased;
frameworkElement.PointerExited += OnPointerExited;
frameworkElement.PointerCanceled += OnPointerCancelled;
}
}
...
}
OnPointerPressed
ハンドラーでは、CommonHandler
メソッドの onTouchAction
フィールドを呼び出すことでエフェクト イベントが呼び出されます。
public class TouchEffect : PlatformEffect
{
...
void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Pressed, args);
// Check setting of Capture property
if (effect.Capture)
{
(sender as FrameworkElement).CapturePointer(args.Pointer);
}
}
...
void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
{
PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
Windows.Foundation.Point windowsPoint = pointerPoint.Position;
onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
touchActionType,
new Point(windowsPoint.X, windowsPoint.Y),
args.Pointer.IsInContact));
}
}
OnPointerPressed
ではまた、.NET Standard ライブラリのエフェクト クラスにある Capture
プロパティの値が確認され、true
の場合、CapturePointer
が呼び出されます。
その他の UWP イベント ハンドラーはさらに単純です。
public class TouchEffect : PlatformEffect
{
...
void OnPointerEntered(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Entered, args);
}
...
}
Android 実装
Android 実装と iOS 実装は指が要素間を移動するときに Exited
イベントと Entered
イベントを実装する必要があるため、必然的に複雑性が上がります。 いずれの実装も構造は似通っています。
Android TouchEffect
クラスでは、Touch
イベントに対してハンドラーがインストールされます。
view = Control == null ? Container : Control;
...
view.Touch += OnTouch;
このクラスにより、2 つの静的ディクショナリも定義されます。
public class TouchEffect : PlatformEffect
{
...
static Dictionary<Android.Views.View, TouchEffect> viewDictionary =
new Dictionary<Android.Views.View, TouchEffect>();
static Dictionary<int, TouchEffect> idToEffectDictionary =
new Dictionary<int, TouchEffect>();
...
OnAttached
オーバーライドが呼び出されるたびに viewDictionary
に新しいエントリが取得されます。
viewDictionary.Add(view, this);
エントリは OnDetached
のディクショナリから削除されます。 エフェクトがアタッチされている特定のビューに TouchEffect
のすべてのインスタンスが関連付けられます。 静的ディクショナリにより、あらゆる TouchEffect
インスタンスで他のすべてのビューとそれに対応する TouchEffect
インスタンスを列挙できます。 これはビュー間でイベントを転送するために必要です。
一本一本の指を追跡することをアプリケーションで可能にする ID コードが Android によってタッチ イベントに割り当てられます。 idToEffectDictionary
によってこの ID コードと TouchEffect
インスタンスが関連付けられます。 指が画面に触れたことで Touch
ハンドラーが呼び出されると、このディクショナリに項目が追加されます。
void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
...
switch (args.Event.ActionMasked)
{
case MotionEventActions.Down:
case MotionEventActions.PointerDown:
FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);
idToEffectDictionary.Add(id, this);
capture = libTouchEffect.Capture;
break;
指が画面から離れると、この項目は idToEffectDictionary
から削除されます。 FireEvent
メソッドでは単純に、OnTouchAction
メソッドを呼び出すために必要なすべての情報が蓄積されます。
void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
// Get the method to call for firing events
Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.libTouchEffect.OnTouchAction;
// Get the location of the pointer within the view
touchEffect.view.GetLocationOnScreen(twoIntArray);
double x = pointerLocation.X - twoIntArray[0];
double y = pointerLocation.Y - twoIntArray[1];
Point point = new Point(fromPixels(x), fromPixels(y));
// Call the method
onTouchAction(touchEffect.formsElement,
new TouchActionEventArgs(id, actionType, point, isInContact));
}
その他の種類のタッチはすべて 2 つの異なる方法で処理されます。Capture
プロパティが true
の場合、タッチ イベントは TouchEffect
情報に極めて単純に変換されたものになります。 Capture
が false
のときは複雑性が上がります。場合によっては、ビュー間でタッチ イベントが動く必要があるためです。 これは CheckForBoundaryHop
メソッドの担当となります。このメソッドは移動中に呼び出されます。 このメソッドでは、両方の静的ディクショナリが利用されます。 viewDictionary
を列挙し、指が現在触れているビューを判断し、idToEffectDictionary
を使用して特定の ID に関連付けられている現行の TouchEffect
インスタンス (したがって、現在のビュー) を保存します。
void CheckForBoundaryHop(int id, Point pointerLocation)
{
TouchEffect touchEffectHit = null;
foreach (Android.Views.View view in viewDictionary.Keys)
{
// Get the view rectangle
try
{
view.GetLocationOnScreen(twoIntArray);
}
catch // System.ObjectDisposedException: Cannot access a disposed object.
{
continue;
}
Rectangle viewRect = new Rectangle(twoIntArray[0], twoIntArray[1], view.Width, view.Height);
if (viewRect.Contains(pointerLocation))
{
touchEffectHit = viewDictionary[view];
}
}
if (touchEffectHit != idToEffectDictionary[id])
{
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, TouchActionType.Exited, pointerLocation, true);
}
if (touchEffectHit != null)
{
FireEvent(touchEffectHit, id, TouchActionType.Entered, pointerLocation, true);
}
idToEffectDictionary[id] = touchEffectHit;
}
}
idToEffectDictionary
に変更がある場合、このメソッドは Exited
と Entered
に対して FireEvent
を呼び出し、ビューを変えることがあります。 ただし、TouchEffect
がアタッチされていないビューに占有されている領域に指が移動することがあります。あるいは、このエフェクトがアタッチされているビューにその領域から移動することがあります。
ビューがアクセスされるときの try
および catch
ブロックに注目してください。 このブロックに進み、それからホーム ページに戻るページでは、OnDetached
メソッドは呼び出されません。項目は viewDictionary
に残りますが、Android では処分済みと見なされます。
iOS 実装
iOS 実装は Android 実装に似ていますが、iOS TouchEffect
クラスによって UIGestureRecognizer
の派生物をインスタンス化する必要があります。 これは TouchRecognizer
という名前の iOS プロジェクトのクラスです。 このクラスは、TouchRecognizer
インスタンスを格納する 2 つの静的ディクショナリを維持します。
static Dictionary<UIView, TouchRecognizer> viewDictionary =
new Dictionary<UIView, TouchRecognizer>();
static Dictionary<long, TouchRecognizer> idToTouchDictionary =
new Dictionary<long, TouchRecognizer>();
この TouchRecognizer
クラスの構造の大部分は Android TouchEffect
クラスと似通っています。
重要
UIKit
のビューの多くは、既定ではタッチが有効になっていません。 タッチを有効にするには、iOS プロジェクトの TouchEffect
クラス内で OnAttached
オーバーライドに view.UserInteractionEnabled = true;
を追加します。 これは、エフェクトがアタッチされている要素に対応するUIView
が取得されてから行う必要があります。
タッチ エフェクトを動かす
サンプル プログラムには、一般的な作業のタッチトラッキング エフェクトをテストするページが 5 つ含まれています。
[BoxView Dragging]\(BoxView ドラッグ操作\) ページでは、BoxView
要素を AbsoluteLayout
に追加し、画面上をドラッグできます。 XAML ファイルによって、BoxView
要素を AbsoluteLayout
に追加し、AbsoluteLayout
を消去するための Button
ビューが 2 つインスタンス化されます。
また、新しい BoxView
を AbsoluteLayout
に追加する分離コード ファイルのメソッドによって、TouchEffect
オブジェクトが BoxView
に追加され、イベント ハンドラーがエフェクトにアタッチされます。
void AddBoxViewToLayout()
{
BoxView boxView = new BoxView
{
WidthRequest = 100,
HeightRequest = 100,
Color = new Color(random.NextDouble(),
random.NextDouble(),
random.NextDouble())
};
TouchEffect touchEffect = new TouchEffect();
touchEffect.TouchAction += OnTouchEffectAction;
boxView.Effects.Add(touchEffect);
absoluteLayout.Children.Add(boxView);
}
TouchAction
イベント ハンドラーによってすべての BoxView
要素に対してすべてのタッチ イベントが処理されますが、いくつかの注意が必要です。1 つの BoxView
に対して 2 本の指は許可されません。このプログラムでは、ドラッグ操作のみが実装されるためです。2 本の指では互いに干渉することがあります。 このため、このページでは、現在追跡されている指ごとに埋め込みクラスが定義されます。
class DragInfo
{
public DragInfo(long id, Point pressPoint)
{
Id = id;
PressPoint = pressPoint;
}
public long Id { private set; get; }
public Point PressPoint { private set; get; }
}
Dictionary<BoxView, DragInfo> dragDictionary = new Dictionary<BoxView, DragInfo>();
dragDictionary
には、現在ドラッグされている BoxView
ごとのエントリが含まれます。
Pressed
タッチ操作によって項目がこのディクショナリに追加され、Released
操作によってそれが削除されます。 その BoxView
のディクショナリに既に項目が存在しないかどうかを Pressed
ロジックで確認する必要があります。 存在する場合、BoxView
は既にドラッグされていて、新しいイベントはその同じ BoxView
で 2 本目の指になります。 Moved
アクションと Released
アクションについては、その BoxView
に対するエントリがディクショナリにあるかどうかとそのドラッグされた BoxView
のタッチ Id
プロパティがディクショナリ エントリのそれと一致するかどうかをイベント ハンドラーで確認する必要があります。
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
BoxView boxView = sender as BoxView;
switch (args.Type)
{
case TouchActionType.Pressed:
// Don't allow a second touch on an already touched BoxView
if (!dragDictionary.ContainsKey(boxView))
{
dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));
// Set Capture property to true
TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
touchEffect.Capture = true;
}
break;
case TouchActionType.Moved:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
Point initialLocation = dragDictionary[boxView].PressPoint;
rect.X += args.Location.X - initialLocation.X;
rect.Y += args.Location.Y - initialLocation.Y;
AbsoluteLayout.SetLayoutBounds(boxView, rect);
}
break;
case TouchActionType.Released:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
dragDictionary.Remove(boxView);
}
break;
}
}
Pressed
ロジックによって TouchEffect
オブジェクトの Capture
プロパティが true
に設定されます。 これにはその指のすべての後続イベントを同じイベント ハンドラーに届けるという効果があります。
BoxView
添付プロパティを変更することで、Moved
ロジックによって LayoutBounds
が移動します。 イベント引数の Location
プロパティは常に、ドラッグされている BoxView
と相対的になります。BoxView
が一定の速度でドラッグされている場合、連続するイベントの Location
プロパティはだいたい同じになります。 たとえば、指が BoxView
の中心を押すと、Pressed
アクションによって PressPoint
プロパティ (50, 50) が格納されます。これは後続イベントに対して同じになります。 BoxView
が一定の速度で斜めにドラッグされた場合、Moved
アクション中の後続の Location
プロパティは値 (55, 55) になることがあります。その場合、Moved
ロジックによって、BoxView
の水平および垂直位置に 5 が加算されます。 これで BoxView
が移動し、その中央が再び指の真下になります。
異なる指を使用し、複数の BoxView
要素を同時に動かすことができます。
ビューのサブクラス化
多くの場合、Xamarin.Forms 要素ではそれ自体のタッチ イベントを簡単に処理できます。 [Draggable BoxView Dragging]\(ドラッグ可能 BoxView ドラッグ操作\) ページの機能は [BoxView Dragging]\(BoxView ドラッグ操作\) ページと同じですが、ユーザーがドラッグする要素は BoxView
から派生した DraggableBoxView
クラスのインスタンスとなります。
class DraggableBoxView : BoxView
{
bool isBeingDragged;
long touchId;
Point pressPoint;
public DraggableBoxView()
{
TouchEffect touchEffect = new TouchEffect
{
Capture = true
};
touchEffect.TouchAction += OnTouchEffectAction;
Effects.Add(touchEffect);
}
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
if (!isBeingDragged)
{
isBeingDragged = true;
touchId = args.Id;
pressPoint = args.Location;
}
break;
case TouchActionType.Moved:
if (isBeingDragged && touchId == args.Id)
{
TranslationX += args.Location.X - pressPoint.X;
TranslationY += args.Location.Y - pressPoint.Y;
}
break;
case TouchActionType.Released:
if (isBeingDragged && touchId == args.Id)
{
isBeingDragged = false;
}
break;
}
}
}
このコンストラクターによって TouchEffect
が作成され、アタッチされ、そのオブジェクトが最初にインスタンス化されたとき、Capture
プロパティが設定されます。 クラス自体が各指に関連付けられている isBeingDragged
、pressPoint
、touchId
値を保管するため、ディクショナリは必要ありません。 DraggableBoxView
の親が AbsoluteLayout
でない場合でもロジックが動作するように、Moved
処理により TranslationX
プロパティと TranslationY
プロパティが変更されます。
SkiaSharp との統合
次の 2 つのデモンストレーションにはグラフィックスが必要です。そのため、SkiaSharp が使用されます。 以下の例を見る前に Xamarin.Forms で SkiaSharp を使用する方法に関するページを読むことをお勧めします。 ここで必要なものはすべて、最初の 2 つの記事 (「SkiaSharp Drawing Basics」 (SkiaSharp 描画の基礎) と「SkiaSharp Lines and Paths」 (SkiaSharp の線とパス)) に記載されています。
[Ellipse Drawing]\(楕円の描画\) ページでは、画面を指でなぞることで楕円を描くことができます。 指の動かし方に基づき、左上から右下に、あるいは任意の他の隅からその反対側の隅に楕円を描くことができます。 楕円は無作為で選択された色と不透明度で描画されます。
楕円の 1 つに触れたら、それを別の場所にドラッグできます。 これには "ヒットテスト" と呼ばれている手法が必要です。ヒットテストでは、特定の点にあるグラフィカル オブジェクトが検索されます。 SkiaSharp の楕円は Xamarin.Forms 要素ではありません。そのため、独自の TouchEffect
処理を実行できません。 TouchEffect
は SKCanvasView
オブジェクト全体に適用する必要があります。
EllipseDrawPage.xaml ファイルによって、シングルセルの Grid
で SKCanvasView
がインスタンス化されます。 TouchEffect
オブジェクトはその Grid
にアタッチされます。
<Grid x:Name="canvasViewGrid"
Grid.Row="1"
BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
Android とユニバーサル Windows プラットフォームの場合、TouchEffect
を SKCanvasView
に直接アタッチできますが、iOS の場合、それは機能しません。 Capture
プロパティが true
に設定されていることに注目してください。
SkiaSharp によってレンダリングされる各楕円が型 EllipseDrawingFigure
のオブジェクトによって表されます。
class EllipseDrawingFigure
{
SKPoint pt1, pt2;
public EllipseDrawingFigure()
{
}
public SKColor Color { set; get; }
public SKPoint StartPoint
{
set
{
pt1 = value;
MakeRectangle();
}
}
public SKPoint EndPoint
{
set
{
pt2 = value;
MakeRectangle();
}
}
void MakeRectangle()
{
Rectangle = new SKRect(pt1.X, pt1.Y, pt2.X, pt2.Y).Standardized;
}
public SKRect Rectangle { set; get; }
// For dragging operations
public Point LastFingerLocation { set; get; }
// For the dragging hit-test
public bool IsInEllipse(SKPoint pt)
{
SKRect rect = Rectangle;
return (Math.Pow(pt.X - rect.MidX, 2) / Math.Pow(rect.Width / 2, 2) +
Math.Pow(pt.Y - rect.MidY, 2) / Math.Pow(rect.Height / 2, 2)) < 1;
}
}
プログラムによってタッチ入力が処理されるとき、StartPoint
プロパティと EndPoint
プロパティが使用されます。楕円の描画には Rectangle
プロパティが使用されます。 楕円がドラッグされるとき、LastFingerLocation
プロパティが使用されます。IsInEllipse
メソッドはヒットテストを支援します。 このメソッドは、点が楕円内にあるとき、true
を返します。
分離コード ファイルには 3 つのコレクションが保持されます。
Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();
draggingFigure
ディクショナリには、completedFigures
コレクションのサブセットが含まれます。 SkiaSharp PaintSurface
イベント ハンドラーによって単純に、completedFigures
コレクションと inProgressFigures
コレクションでオブジェクトがレンダリングされます。
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Fill
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (EllipseDrawingFigure figure in completedFigures)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
foreach (EllipseDrawingFigure figure in inProgressFigures.Values)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
}
タッチ処理の最も巧妙な所は Pressed
処理です。 ここはヒットテストが実行される箇所ですが、ユーザーの指の下に楕円があることがコードによって検出された場合、別の指で現在ドラッグされていない場合にのみ、その楕円をドラッグできます。 ユーザーの指の下に楕円がない場合、新しい楕円の描画プロセスがコードによって開始されます。
case TouchActionType.Pressed:
bool isDragOperation = false;
// Loop through the completed figures
foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>())
{
// Check if the finger is touching one of the ellipses
if (fig.IsInEllipse(ConvertToPixel(args.Location)))
{
// Tentatively assume this is a dragging operation
isDragOperation = true;
// Loop through all the figures currently being dragged
foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values)
{
// If there's a match, we'll need to dig deeper
if (fig == draggedFigure)
{
isDragOperation = false;
break;
}
}
if (isDragOperation)
{
fig.LastFingerLocation = args.Location;
draggingFigures.Add(args.Id, fig);
break;
}
}
}
if (isDragOperation)
{
// Move the dragged ellipse to the end of completedFigures so it's drawn on top
EllipseDrawingFigure fig = draggingFigures[args.Id];
completedFigures.Remove(fig);
completedFigures.Add(fig);
}
else // start making a new ellipse
{
// Random bytes for random color
byte[] buffer = new byte[4];
random.NextBytes(buffer);
EllipseDrawingFigure figure = new EllipseDrawingFigure
{
Color = new SKColor(buffer[0], buffer[1], buffer[2], buffer[3]),
StartPoint = ConvertToPixel(args.Location),
EndPoint = ConvertToPixel(args.Location)
};
inProgressFigures.Add(args.Id, figure);
}
canvasView.InvalidateSurface();
break;
もう 1 つの SkiaSharp 例は [Finger Paint] ページです。 2 つの Picker
ビューからストロークの色と幅を選択し、1 本または複数の指で描画できます。
この例では、画面にペイントされる各線を表す個別のクラスも必要になります。
class FingerPaintPolyline
{
public FingerPaintPolyline()
{
Path = new SKPath();
}
public SKPath Path { set; get; }
public Color StrokeColor { set; get; }
public float StrokeWidth { set; get; }
}
各線をレンダリングするために SKPath
オブジェクトが使用されます。 FingerPaint.xaml.cs ファイルには、このようなオブジェクトのコレクションが 2 つ保持されます。1 つは現在描画されているポリライン用のコレクションで、もう 1 つは完成したポリライン用のコレクションです。
Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();
Pressed
処理によって新しい FingerPaintPolyline
が作成され、最初の点を保存するためにパス オブジェクトで MoveTo
が呼び出され、そのオブジェクトが inProgressPolylines
ディクショナリに追加されます。 Moved
処理によって、指の新しい位置に基づいてパス オブジェクトで LineTo
が呼び出され、Released
処理によって、完成したポリラインが inProgressPolylines
から completedPolylines
に移ります。 繰り返しになりますが、実際の SkiaSharp 描画コードは比較的単純です。
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (FingerPaintPolyline polyline in completedPolylines)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
}
ビュー間タッチの追跡
これまでの例ではすべて、TouchEffect
が作成されたか、Pressed
イベントが発生したとき、TouchEffect
の Capture
プロパティが true
に設定されました。 これにより、最初にビューを押した指に関連付けられているすべてのイベントが同じ要素に受け取られます。 最後のサンプルでは、Capture
が true
に設定されていません。 それにより、画面に触れている指が要素間を動くとき、さまざまな動作が引き起こされます。 Type
プロパティが TouchActionType.Exited
に設定されたイベントを指の移動元の要素が受け取り、Type
が TouchActionType.Entered
に設定されたイベントを 2 つ目の要素が受け取ります。
この種類のタッチ処理は音楽用キーボードで非常に便利です。 鍵盤は押されたタイミングだけでなく、指が鍵盤間をスライド移動したタイミングも検出できなければなりません。
[Silent Keyboard]\(サイレント キーボード\) ページでは、BoxView
から派生する Key
から派生する小さな WhiteKey
クラスと BlackKey
クラスが定義されます。
Key
クラスは、実際の音楽プログラムですぐに使用できます。 このクラスによって IsPressed
および KeyNumber
という名前のパブリック プロパティが定義されますが、後者は MIDI 標準で決められている鍵盤コードに設定されます。 Key
クラスによって StatusChanged
という名前のイベントも定義されます。このイベントは、IsPressed
プロパティが変更されたときに呼び出されます。
各キーでは複数の指が許可されます。 そのため、Key
クラスでは、そのキーに現在触れているすべての指のタッチ ID 番号の List
が保持されます。
List<long> ids = new List<long>();
TouchAction
イベント ハンドラーによって、イベントの種類である Pressed
と Entered
の両方に対して ids
リストに ID が追加されますが、Entered
の場合、IsInContact
プロパティが true
のときに限られます。 Released
イベントと Exited
イベントの場合、ID が List
から削除されます。
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
AddToList(args.Id);
break;
case TouchActionType.Entered:
if (args.IsInContact)
{
AddToList(args.Id);
}
break;
case TouchActionType.Moved:
break;
case TouchActionType.Released:
case TouchActionType.Exited:
RemoveFromList(args.Id);
break;
}
}
AddToList
メソッドと RemoveFromList
メソッドの両方で、List
が空から空ではない状態に (あるいはその逆に) 変化したかどうかが確認され、変化した場合、StatusChanged
イベントが呼び出されます。
さまざまな WhiteKey
および BlackKey
要素がページの XAML ファイル に配置されます。電話を横に構えると見栄えが良くなります。
鍵盤で指をすべらすと、タッチ イベントが鍵盤間を移動することが微妙な色の変化によりわかります。
まとめ
この記事では、エフェクトでイベントを呼び出す方法と、低レベルのマルチタッチ処理を実装するエフェクトを記述し、使用する方法を紹介しました。