読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Xamarin.Forms ジェスチャー

【 Xamarin 記事一覧 】

この記事は、Xamarin Advent Calendar 2014 - Qiitaの21日目の記事です。
←20日目の記事:XamarinでもF# - omanuke-ekunamoの日記

Xamarin.Formsでのタップの取得

モバイル端末でのプログラムでは、タップ・スワイプなど、各種のジェスチャーに対応することがあります。
本記事では、Xamarin.Formsからジェスチャーを処理する要領をまとめてみたいと思います。

タップ

f:id:furuya02:20141213044647p:plain:w150 f:id:furuya02:20141213044646p:plain:w150

Xamarin.Formsには、TapGestureRecognizerというクラスが提供されており、これを
コントロールのGestureRecognizersコレクションに追加することで、当該コントロール上で
検出したジェスチャー(タップ)を簡単に取得できます。

下記のコートは、Imageコントロール上でのタップを処理するコードです。

public MyPage() {

    var image = new Image {
       HeightRequest = 200,
       Source = ImageSource.FromResource("App1.Images.pronama.png")//プロ生ちゃん画像を表示
    };
    
    //ジェスチャレコナイザを生成する
    var gr = new TapGestureRecognizer();
    gr.Tapped += (s, e) => { //タップされた時のイベントを記述する
        DisplayAlert("", "Tap", "OK");//アラート表示
    };
    //イメージコントロールにジェスチャレコナイザを追加する
    image.GestureRecognizers.Add(gr);

    Content = new StackLayout {
        //iOSで上余白を確保
        Padding = new Thickness(0,Device.OnPlatform(20,0,0),0,0),
        Children = { image }
    };
}


※イベントTappedは、Xamarin.Formsの最新Stable 1.2.3.6257でないと使用できません
1.0.6186の場合
f:id:furuya02:20141213045248p:plain
1.2.3.6257の場合
f:id:furuya02:20141213045249p:plain

ダブルタップ

f:id:furuya02:20141213045601p:plain:w150 f:id:furuya02:20141213045603p:plain:w150

TapGestureRecognizerのNumberOfTapsRequiredには、タップ数をセットできます。
ここに2を設定することでダブルタップの検出になります。

    //TapGestureRecognizerを生成する
    var gr = new TapGestureRecognizer();
    gr.NumberOfTapsRequired = 2;//タップ数を2に設定する
    gr.Tapped += (s, e) => { //タップされた時のイベントを処理する
        DisplayAlert("", "Double Tap", "OK");//アラート表示
    };

ここまでの要求であれば、Xamarin.FormsのPCLだけで超簡単に記述できます。しかし、それこれ以上の要件になると、残念ながら、PCLだけでは対応できません。

シングルタップとダブルタップの区別

Xamarin.FormsのTapGestureRecognizerには、シングルタップとダブルタップを区別する方法がありません。(ダブルタップの発生時にシングルタップも検出してしまいます)
仕方がないので、ここから先はレンダラで記述することになります。

iOSの場合

f:id:furuya02:20141213045833p:plain:w150 f:id:furuya02:20141213045830p:plain:w150 f:id:furuya02:20141213045832p:plain:w150

iOSでは、UITapGestureRecognizerでタップイベントを処理できます。また、NumberOfTapsRequiredで、検出対象とするタップ回数を指定することもできます。
なお、シングルタップとダブルタップの区別は、RequireGestureRecognizerToFailを使用する事で可能になっています。

//App1.iOS.ExImageRenderer.cs
[assembly: ExportRenderer(typeof(ExImage), typeof(ExImageRenderer))]
namespace App1.iOS {
    internal class ExImageRenderer : ImageRenderer {

        private UITapGestureRecognizer _singleTap;
        private UITapGestureRecognizer _doubleTap;

        protected override void OnElementChanged(ElementChangedEventArgs<Image> e) {
            base.OnElementChanged(e);

            //ExImageへのポインタ取得
            var o = Element as ExImage;

            if (o != null) {
                _singleTap = new UITapGestureRecognizer(() => o.OnSingleTap()); //イベント発生時に、ExImageのOnSingleTapを呼び出す
                _doubleTap = new UITapGestureRecognizer(() => o.OnDoubleTap()); //イベント発生時に、ExImageのOnDoubleTapを呼び出す
                _doubleTap.NumberOfTapsRequired = 2;//タップ数は2回
                _singleTap.RequireGestureRecognizerToFail(_doubleTap);//ダブルタップに失敗した時、シングルタップを処理する

                //ジェスチャーレコナイザーの追加
                RemoveGestureRecognizer(_singleTap);
                RemoveGestureRecognizer(_doubleTap);
            }
            
            if (e.OldElement == null) {
                //ジェスチャーレコナイザーの解放
                AddGestureRecognizer(_singleTap);
                AddGestureRecognizer(_doubleTap);
            }

        }
    }
}

Androidの場合

f:id:furuya02:20141213050434p:plain:w150 f:id:furuya02:20141213050431p:plain:w150 f:id:furuya02:20141213050433p:plain:w150


まず最初にGestureDetector.SimpleOnGestureListenerを継承して、リスナークラスを定義します。
リスナークラスでは、イベント発生時に、元のコントロールであるExImageへのポインタを確保し、OnSingleTapやOnDoubleTapを呼び出しています。
なお、SimpleOnGestureListenerでは、ダブルタップ時にOnDoubleTapEventが2回発生します。そのこで、その前にOnDoubleTapが発生しているかどうかを処理判断(_doubleTap)として重複発生を防止しています。

//App1.Android.MyGestureListener.cs
//リスナー(SimpleOnGestureListener)クラスを生成する
    public class MyGestureListener : GestureDetector.SimpleOnGestureListener {
        
        //ExImageへのポインタ
        public ExImage ExImage { get; set; }

        private bool _doubleTap = false;//OnDoubleTapEventが2回処理されてしまうのを防止するためのフラグ
        
        public override bool OnDoubleTap(MotionEvent e) {
            _doubleTap = true;//ダブルタップが発生したことフラグに記録する
            return base.OnDoubleTap(e);
        }

        public override bool OnSingleTapConfirmed(MotionEvent e) {
            ExImage.OnSingleTap();//ExImageのOnSingleTapを呼び出す
            return base.OnSingleTapConfirmed(e);
        }
        public override bool OnDoubleTapEvent(MotionEvent e) {
            if (_doubleTap) {//フラグが立っている時だけ処理する
                _doubleTap = false;
                ExImage.OnDoubleTap();//ExImageのOnDoubleTapを呼び出す
            }
            return base.OnDoubleTapEvent(e);
        }

    }

続いて、リスナー(SimpleOnGestureListener)のインスタンスをとおしてGestureDetectorを生成し、
コントロールに登録するコードです。

//App1.Android.ExImageRenderer.cs
[assembly: ExportRenderer(typeof(ExImage), typeof(ExImageRenderer))]
namespace App1.Droid
{
    class ExImageRenderer:ImageRenderer{
        private readonly MyGestureListener _listener;
        private readonly GestureDetector _detector;

        public ExImageRenderer() {
            //リスナー(SimpleOnGestureListener)クラスを生成する
            _listener = new MyGestureListener();
            //リスナー(SimpleOnGestureListener)のインスタンスを渡してGestureDetectorを生成する。
            _detector = new GestureDetector(_listener);
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Image> e) {
            base.OnElementChanged(e);

            _listener.ExImage = Element as ExImage;
            
            if (e.NewElement == null) {
                GenericMotion -= HandleGenericMotion;
                Touch -= HandleTouch;
            }

            if (e.OldElement == null) {
                GenericMotion += HandleGenericMotion;
                Touch += HandleTouch;
            }
        }
        void HandleTouch(object sender, TouchEventArgs e) {
            _detector.OnTouchEvent(e.Event);
        }

        void HandleGenericMotion(object sender, GenericMotionEventArgs e) {
            _detector.OnTouchEvent(e.Event);
        }
    }
}

WindowsPhoneの場合

f:id:furuya02:20141213050657p:plain:w150 f:id:furuya02:20141213050654p:plain:w150 f:id:furuya02:20141213050655p:plain:w150

WindowsPhoneでもAndroidと同じような仕組みで、GestureListenerでタップを検出できます。
ただし、GestureListenerは、ダブルクラップでも必ずシングルタップのイベントが発生してしまうので、ちょっとトリッキーですが、
フラグを使用して、その区別を埋め込んでいます。

//App1.WinPhone.ExImageRenderer.cs
[assembly: ExportRenderer(typeof(ExImage), typeof(ExImageRenderer))]
namespace App1.WinPhone{
    class ExImageRenderer:ImageRenderer {

        private bool _singleTap = false;//シングルタップを無効化するためのフラグ

        protected override void OnElementChanged(ElementChangedEventArgs<Image> e) {
            base.OnElementChanged(e);

            var o = Element as ExImage;
            if (o != null) {
                //GestureListenerの取得
                var gl = GestureService.GetGestureListener(this);

                //イベント処理
                gl.Tap += async (sender, args) => {
                    _singleTap = true;//とりあえずシングルタップ発生のフラグを立てる
                    await Task.Delay(200);//少し待機する
                    if (_singleTap) {//待機中にフラグが取り消されていない場合は、シングルタップとして処理する
                        o.OnSingleTap();//イベント発生時に、ExImageのOnSingleTapを呼び出す
                    }

                };
                gl.DoubleTap += (sender, args) => {
                    _singleTap = false;//ダブルタップの発生なので、シングルタップは無効化する
                    o.OnDoubleTap();//イベント発生時に、ExImageのOnDoubleTapを呼び出す
                };
            }

        }
    }

}

その他のジェスチャー処理

レンダラーを紹介した段階で、察しのいい方は、もう分かっていると思うのですが・・・
そう、レンダラーでそれぞれのプラットフォームに装備されているジェスチャー取得の仕組みを使えば、そのままXamarin.Formsで利用可能だという事です。

iOS

iOSのレンダラーで使用したUITapGestureRecognizerは、同じように利用が可能なクラスが他にも提供されていますので、列挙しておきます。

//クラス名がUIで始まり、estureRecognizerで終わるものを列挙する
Assembly asm = typeof(UITapGestureRecognizer).GetTypeInfo().Assembly;
var ar = new List<String>();
// エクスポートの一覧取得
foreach (var type in asm.ExportedTypes) {
    if (type.GetTypeInfo().IsPublic && !type.GetTypeInfo().IsInterface) {
        if (type.Name.IndexOf("UI") != 0) {//UIで始まるか?
            continue;
        }
        //GestureRecognizerで終わるか?
        var name = "GestureRecognizer";
        if (type.Name.Length < name.Length || type.Name.IndexOf(name) != type.Name.Length - name.Length) {
            continue;
        }
        ar.Add(type.Name);
    }
}

f:id:furuya02:20141213063443p:plain

UITapGestureRecognizer(タップ)
UIPinchGestureRecognizer(ピンチ)
UIPanGestureRecognizer(パン・ドラッグ)
UISwipeGestureRecognizer(スワイプ)
UIRotationGestureRecognizer(ローテイト)
UILongPressGestureRecognizer(ロングプレス)

Android

f:id:furuya02:20141213053019p:plain
Androidのレンダラーで使用したSimpleOnGestureListenerには、下記のようなイベントが実装されています。

OnDown(押下)
OnShowPress(押下 [押してすぐに動かすと呼ばれない] )
OnLongPress(長押し)
OnFling(フリック)
OnScroll(スクロール)
OnSingleTapUp(シングルタップ [ダブルタップ時も呼ばれる] )
OnSingleTapConfirmed(シングルタップ [ダブルタップ時は呼ばれない] )
OnDoubleTap(ダブルタップ)
OnDoubleTapEvent(ダブルタップ [押す・動かす・離す] )

Windows Phone

f:id:furuya02:20141213054203p:plain

WindowsPhoneのGestureListenerには、次のようなイベントが実装されています。


DoubleTap(ダブルタップ)
DragCompleted(ダブルタップ終了)
DragDelta(ドラッグ中)
DragStarted(ドラッグ開始)
Flick(フリック)
GestureBegin(ジェスチャー開始)
GestureCompleted(ジェスチャー完了)
Hold(ホールド [タッチして 1 秒間押し続ける] )
PinchCompleted(ピンチ操作終了)
PinchDelta(ツー タッチ ポイント (2 本指) 操作)
PinchStarted(ピンチ開始)
Ta(タップ)

ジェスチャーの利用

各プラットフォームごとジェスチャー処理の方法(解釈)が微妙に違いますので、共通事項を括りだしたり、汎用的なライブラリを作成するのは、ちょっと骨が折れそうです。この辺が、Xamarni.Formsで実装されてない理由でもあるのでしょう。
したがって、必要な場面で必要なジェスチャーを取得するコードを記述する必要があるということでしょう。

サンプルコードは、先のレンダラーもので簡単に応用がきくと無いと思いますので省略いたしますが、決して力尽きたわけではありませんw

参考にさせて頂いたページ

Xamarinでコントロールのドラッグに対応する | Moonmile Solutions Blog
Xamarin.iOS でジェスチャを認識する - Qiita
Gesture Recognizers with Xamarin.Forms
http://developer.xamarin.com/guides/cross-platform/xamarin-forms/working-with/gest
Touch | Xamarin

※画像は、プロ生ちゃんで公開されている壁紙を利用させて頂きました。

プロ生ちゃん(暮井 慧) | プログラミング生放送


【 Xamarin 記事一覧 】