Xamarin.Forms でドラッグを実装しよう(のうりん編)
http://www.moonmile.net/blog/archives/7653
Xamarin.Forms でドラッグを実装しよう(Android編)
http://www.moonmile.net/blog/archives/7667
の続きで、前回の Xamarin.Android を Xamarin.Forms で動くようにします。
Android だけの場合であれば Xamarin.Android で閉じてしまえばよいのですが、iOS も同時に動かしたいのと、View に XAML の構文を使いたい(最終的には MVVM パターンに落とし込みたい)ので、Xamarin.Forms を使います。
ただし、ここでは Android の Touch イベントを持ってくるだけなので iOS では動きません…が、インターフェースは揃えておきましょう。
本来ならば、
Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)
http://www.buildinsider.net/mobile/xamarintips/0035
にあるように GestureDetector.SimpleOnGestureListener のスタイルを取りたいところなのですが、なぜかタップ位置が消えてしまっているので自作をします。このリスナークラスには GestureDetector.IOnGestureListener というインターフェースが用意されているので、頑張ればこのスタイルに沿って自作できないこともないのですが…どうも、なんか回りくどいので、別な方式をとります。
Xamarin.Forms の場合、PCL にあるコントロールと、それぞれのプラットフォームにあるコントロールのレンダラーがワンセットになっているので、直接レンダラーから PCL のコントロールにコールバックしてしまいます。こうすると無駄な Listener クラスが無くなって構造が簡単になります。まあ、Listener パターンは汎用性を広げるために使っているので、場合によりけりということです。
サンプルコード
サンプルコードは、https://github.com/moonmile/BoxDrag にあります。
BoxDragXF フォルダが Xamarin.Forms で作ったテストアプリ一式になります。
PCL に MainPage.xaml を作る
共通化される PCL プロジェクトに MainPage.xaml を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <? 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:local = "clr-namespace:BoxDragXF;assembly:BoxDragXF" x:Class = "BoxDragXF.MainPage" > < StackLayout > < Button Text = "初期化" x:Name = "button1" ></ Button > < Grid > < Grid.ColumnDefinitions > < ColumnDefinition Width = "1*" /> < ColumnDefinition Width = "1*" /> < ColumnDefinition Width = "1*" /> < ColumnDefinition Width = "1*" /> </ Grid.ColumnDefinitions > < Button Text = "上" x:Name = "buttonUp" ></ Button > < Button Text = "下" x:Name = "buttonDown" Grid.Column = "1" ></ Button > < Button Text = "左" x:Name = "buttonLeft" Grid.Column = "2" ></ Button > < Button Text = "右" x:Name = "buttonRight" Grid.Column = "3" ></ Button > </ Grid > < AbsoluteLayout x:Name = "layout" BackgroundColor = "Blue" HorizontalOptions = "FillAndExpand" VerticalOptions = "FillAndExpand" > < local:BoxViewEx BackgroundColor = "Red" x:Name = "box1" WidthRequest = "60" HeightRequest = "60" AbsoluteLayout.LayoutBounds = "100,100,60,60" /> </ AbsoluteLayout > </ StackLayout > </ ContentPage > |
ドラッグ対象のコントロールは BoxView を継承した BoxViewEx コントロールを使います。このカスタムコントロールを参照するために、”clr-namespace:BoxDragXF;assembly:BoxDragXF” でロードできるようにします。
最初の BoxViewEx クラスは BoxView を継承しただけの空っぽのクラスです。
1 2 3 | public class BoxViewEx : BoxView { } |
Android プロジェクトにレンダラーを追加する
BoxViewEx コントロールに対応するレンダラーを追加します。レンダラーは各プラットフォームの描画をすると同時に、各プラットフォーム特有の動作を記述できます。
最初の BoxExRenderer クラスは、こんな感じになります。
1 2 3 4 5 6 7 8 9 10 11 | [assembly: ExportRenderer( typeof (BoxViewEx), typeof (BoxExRenderer))] namespace BoxDragXF.Droid { class BoxExRenderer : BoxRenderer { protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e) { base .OnElementChanged(e); } } } |
ExportRenderer 属性は、PCL 側にあるコントロールの本体とレンダラーを結び付ける属性で記述が必要です。
この時点で、一度ビルドと実行をして、エラーが無いようにしておきます。
- XAML の local:BoxViewEx の記述
- BoxViewEx クラスの記述
- BoxExRenderer クラスの記述
の3つがワンセットになります。
レンダラーに、タッチイベントを追加する
Xamarin.Android で作成した Touch イベントをそのまま移植します。
レンダラーから、本体のコントロールクラスを参照するときは Element プロパティを参照します。ここでは BoxView クラスになっていますが、実体は BoxViewEx です。
Android で描画する View オブジェクトは、sender として渡されてくるので、これを Android.Views.View にキャストをします。もちろん、必要であれば対応する Android の View コントロールにキャストして大丈夫です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class BoxExRenderer : BoxRenderer { protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e) { base .OnElementChanged(e); this .Touch += BoxExRenderer_Touch; } float _gx, _gy; // 初期の相対値 float _ox, _oy; // 前回の絶対位置 /// <summary> /// タッチイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BoxExRenderer_Touch( object sender, TouchEventArgs e) { var box = sender as Android.Views.View; switch (e.Event.Action) { case MotionEventActions.Down: // 初期の相対値を保存 _gx = e.Event.GetX(); _gy = e.Event.GetY(); break ; case MotionEventActions.Move: // 移動距離を計算 float dx = e.Event.RawX - _ox; float dy = e.Event.RawY - _oy; // コールバック呼び出し // TODO: delta 方式なのか誤差が大きい var el = this .Element as BoxViewEx; el.OnManipulationDelta(el, new ManipulationDeltaRoutedEventArgs(sender, dx, dy)); break ; case MotionEventActions.Up: break ; } // 現在の絶対位置を保存 _ox = e.Event.RawX; _oy = e.Event.RawY; } } |
先の Xamarin.Android と異なるのは、PCL 側にある BoxViewEx クラスの OnManipulationDelta メソッドを直接呼び出していることです。ここでリスナーパターンを使うこともできるのですが、面倒なので直接メソッドを呼び出します。C# の場合は event を使ってもよいでしょう。
引数として渡す ManipulationDeltaRoutedEventArgs クラスは共通で利用する PCL プロジェクトに記述します。このクラスはプラットフォームに依存しないように作ります。名前が UWP 風にしてあるのは、最終的に Windows ユニバーサルアプリ風に MVVM を作るためです。
PCL プロジェクト内の BoxViewEx クラスでドラッグ処理をする
疑似的な ManipulationDeltaRoutedEventArgs クラスを作っておいて、Android 側から通知される OnManipulationDelta メソッドを用意しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public class BoxViewEx : BoxView { public virtual void OnManipulationDelta( object sender, ManipulationDeltaRoutedEventArgs e) { var rc = this .Bounds; rc.X += e.Delta.Translation.X; rc.Y += e.Delta.Translation.Y; this .Layout(rc); } } public class ManipulationDeltaRoutedEventArgs { public ManipulationDeltaRoutedEventArgs( object source, double deltaX, double deltaY) { this .OriginalSource = source; this .Delta = new Delta_() { Translation = new Delta_.Translation_() { X = deltaX, Y = deltaY } }; } public Delta_ Delta { get ; set ; } public object OriginalSource { get ; set ; } public class Delta_ { public Translation_ Translation { get ; set ; } public class Translation_ { public double X { get ; set ; } public double Y { get ; set ; } } } } |
Xamarin.Forms 側でコントロールの位置設定は Layout メソッドを使えば ok です。移動距離が Delta.Translation.X,Y として渡されるのは UWP と同じです。というか、そういう風に作ります。
実行してみると
実行してみると、マウスの位置とコントロールがかなりズレていますがきちんとドラッグできていることがわかります。このずれは、Xamarin.Android の時と同じように差分移動をさせているための誤差です。
デバッグ出力をさせるとわかりますが、Touch イベントが Xamarin.Android の時よりも頻繁に入ります。このためなのか、Android 側の GetX,Y の値が float 型であるものの、Xamarin.Forms が double 型になっていること、最終的な描画(レンダラーのほう)は int 型になっていることなどに影響されていると思います。このあたり、誤差が出ない方式をあとで考えていきます。
ちなみに UWP(Windows のユニバーサルアプリ)で Delta.Translation を使ったときはずれが発生しません。できることならば、うまく調節していきたいところです。
double moveRate = 1.5;
float dx = (e.Event.RawX – _ox) / (float)moveRate;
float dy = (e.Event.RawY – _oy) / (float)moveRate;
移動距離が1.5倍されているようです。