Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Xamarin逆引きTips

Xamarin逆引きTips

Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)

2015年2月18日

iOS/Androidにおけるタップやスワイプなどの各種ジェスチャーを、Xamarin.Formsで処理する方法を解説する。

古谷 誠進(@furuya02
  • このエントリーをはてなブックマークに追加

 モバイルアプリでは、タップやスワイプなど、各種のジェスチャーに対応させる必要がある。今回は、Xamarin.Formsで各種のジェスチャーを処理する方法を解説する*1

  • *1 なお本Tipsは、Windows上でVisual Studio 2013を使用してXamarin.Forms開発をすることを前提としている(編集部注: Mac上のXamarin Studioでも同様の手順で、本稿の内容が実現できることは確認している)。使用しているXamarin.Formsのバージョンは、プロジェクト作成時に利用されている「1.3.1.6296」である。

1. シナリオ

 最初に、画面にImageビューを配置し、これをタップしたときのイベントを処理する。続いて、Xamarin.Formsで処理できないイベントであるロングタップ(長押し)を、レンダラーの実装によって取り扱う方法を解説する。

2 .Xamarin.Formsプロジェクトを作成する

 メニューバーの[ファイル]-[新規作成]-[プロジェクト]から表示したダイアログで、[テンプレート]-[Visual C#]-[Mobile Apps]-[Blank App (Xamarin.Forms Portable)]を選択し、名前を「GestureSample」として[OK]ボタンを押す(図1)。

図1 「Blank App (Xamarin.Forms Portable)」の新規作成

3. Imageビューの表示

 共通プロジェクトに「Images」フォルダーを作成し、表示する画像(本稿のサンプルでは「image01.png」)をその中にコピーする。

図2 共有プロジェクトに1つの画像ファイルを置く

図2 共有プロジェクトに1つの画像ファイルを置く

【今回使用した素材画像】プロ生ちゃん

 サンプルで使用させていただいた画像は、「プロ生」で公開されている壁紙である。

「© 2011-2015 プログラミング生放送」
「© 2011-2015 プログラミング生放送」

 「プロ生」では、ガイドラインに従うことで、素材の利用が可能である。

 続いて、画像ファイルの[プロパティ]で、[ビルド アクション]を「埋め込まれたリソース」に変更する(図3)。

図3 [ビルド アクション]を「埋め込まれたリソース」に変更
図3 [ビルド アクション]を「埋め込まれたリソース」に変更

 画面にImageビューを表示するには、App.csファイルを以下のように修正する。

C#
namespace GestureSample{
  public class App : Application{
    public App(){
      MainPage = new MyPage();
    }

    ……省略……

  }

  internal class MyPage : ContentPage {
    public MyPage() {
      var image = new Image { // ←1
        HeightRequest = 200,
        Source = ImageSource.FromResource("GestureSample.Images.image01.png") //2
      };

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

}
Imageビューを表示するコード(App.cs)

 Sourceプロパティに画像リソースを指定してImageビューを作成する(1)。

 共有プロジェクトに置いたリソースは、2のようにImageSource.FromResource()メソッドで取得できる。このとき、リソース名は、「<プロジェクト名>.<フォルダー名>.<画像ファイル名>」である。

 ビューにはスタックレイアウト(StackLayout)を配置し、そこに画像イメージを表示した(3)。 

 このコードを実行すると次の画面のようになる。

図4 Imageビューを表示(iOS) 図4 Imageビューを表示(Android)
図4 Imageビューを表示(iOS/Android)

4. タップ

 Xamarin.Formsには、TapGestureRecognizerというクラスがあり、これをビューのGestureRecognizersコレクションに追加することで、当該ビューで検出したタップを簡単に取得できる。

 タップイベントを処理するには、App.csファイルを以下のように修正する。

C#
internal class MyPage : ContentPage {
  public MyPage() {
    var image = new Image{ 
      HeightRequest = 200,
      Source = ImageSource.FromResource("GestureSample.Images.image01.png") 
    };

    var gr = new TapGestureRecognizer(); // ←1
    gr.Tapped += (s, e) => {
      DisplayAlert("", "Tap", "OK"); //←2
    };
    image.GestureRecognizers.Add(gr); //←3

    Content = new StackLayout {
      Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0),
      Children = {image}
    };
  }
}
Imageコントロールを配置し、タップイベントを処理するコード(App.cs)

 最初に、TapGestureRecognizerオブジェクトを作成する(1)。

 TapGestureRecognizerオブジェクトのTappedイベントで、アラートダイアログを表示する(2)。

 続いてImageビューのGestureRecognizersコレクションに、作成したTapGestureRecognizerオブジェクトを追加する(3)。

 このコードを実行して、Imageビューをタップすると次のような画面になる。

図5 タップ後の画面(iOS) 図5 タップ後の画面(Android)
図5 タップ後の画面(iOS/Android)

【コラム】AndroidにおけるNumberOfTapsRequiredのバグ

 TapGestureRecognizerクラスには、検出対象とするタップ回数が指定できるNumberOfTapsRequiredプロパティがある。これを利用すると、次のようなコードで簡単にダブルタップが処理できるはずである。

C#
var gr = new TapGestureRecognizer(); 
gr.NumberOfTapsRequired = 2; // 2回のタップを検出対象にする
gr.Tapped += (s, e) => { 
  DisplayAlert("", "Double Tap", "OK");
};
ダブルタップを検出するコード

 実際、iOSで、このコードは正常に動作する。しかし残念ながら、Androidでは、現在、このコードは正常に動作しない。

 この問題は、Xamarin TeamのCraig Dunn氏も「すでにバグとして認識している」と発言しているが、まだ修正はされていない(2015年2月9日時点での最新stableであるXamarin.Forms 1.3.3.6323でも、修正されていないことを確認している)。

5. ロングタップ

 実は、TapGestureRecognizerクラスで取得できるイベントは、タップのみである。Xamarin.Formsで、その他のジェスチャーなどに対応するためには、レンダラーを記述するしか方法はない。

 レンダラーを記述するために必要な最初の作業は、Imageビューを継承した拡張クラス(本稿の例ではExImageクラス)の作成である。GestureSampleプロジェクトに、ExImage.csファイルを追加し、以下のコードのように実装する。

C#
using System;
using Xamarin.Forms;

namespace GestureSample {
  public class ExImage : Image { // ←1

    public event EventHandler LongPress; // ← 2

    public void OnLongPress() { // ← 3
      if (LongPress != null) {
        LongPress(this, new EventArgs());
      }
    }
  }
}
Imageクラスを継承してExImageクラスを作成する(ExImage.cs)

 ExImageクラスは、Imageクラスを継承して作成する(1)。

 2では、ロングタップのイベントを定義する。

 また、publicでOnLongPressメソッドを定義しておき(3)、実際にコントロールでイベントが発生したときに、これを呼び出すようにする。

 続いて、この拡張クラスを使用するようにApp.csファイルを修正する。

C#
using Xamarin.Forms;

namespace GestureSample {

  ……省略……

  internal class MyPage : ContentPage {
    public MyPage() {

      var exImage = new ExImage { // ←1
        HeightRequest = 200,
        Source = ImageSource.FromResource("GestureSample.Images.image01.png")
      };

      exImage.LongPress += (s, a) => { // ←2
        DisplayAlert("", "Long Press", "OK");
      };

      Content = new StackLayout {
        Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0), //iOSで上余白を確保
        Children = {exImage 
      };
    }
  }
}
拡張したImageクラスを使用して、ロングタップを検出するコード(App.cs)

 Imageビューは、拡張したExImageクラスを使用するように変更した(1)。

 また、拡張クラスで新たに定義したLongPressイベントでアラートを表示するようにした(2)。

 その他は、タップのときのコードと同じである。

6. レンダラーの実装(iOS)

 続いて、レンダラーの実装を行う。

 GestureSample.iOSプロジェクトに、ExImageRenderer.csファイルを追加し、以下のコードを記述する。

C#
using GestureSample;
using GestureSample.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(ExImage), typeof(ExImageRenderer))] // ←1
namespace GestureSample.iOS {
  internal class ExImageRenderer : ImageRenderer { // ← 2
    protected override void OnElementChanged(ElementChangedEventArgs<Image> e) {
      base.OnElementChanged(e);

      var exImage = Element as ExImage; 
      var gr = new UILongPressGestureRecognizer(o => exImage.OnLongPress()); // 3

      AddGestureRecognizer(gr); // ← 4
    }
  }
}
iOS用のレンダラー実装コード(ExImageRenderer.cs)

 1は、レンダラーを記述する場合の定型句である。ExportRenderer属性によって、「ExImageコントロールの描画にはExImageRendererクラスを使用する」と定義している。

 ImageビューのレンダラークラスはImageRendererであるので、ExImageRendererクラスは、これを継承している(2)。

 iOSでのロングタップ検出は、UILongPressGestureRecognizerというクラスのインスタンスを生成して(3)、レンダラーのAddGestureRecognizerメソッドでそれをコントロールに登録する(4)という作業になる。

 より詳しく説明すると、3で、UILongPressGestureRecognizerクラスのインスタンスを生成している。そのコンストラクターのメソッド引数に指定しているメソッド(ラムダ式)は、ロングタップを認識(Recognize)すると呼び出される。その中で、先ほど定義したExImageコントロールのOnLongPressメソッドを呼び出している。

 4では、このUILongPressGestureRecognizerオブジェクトを、コントロール自身に登録している。

 このコードを実行して、Imageビューをロングタップすると、次のような画面になる。

図6 ロングタップ後の画面(iOS)
図6 ロングタップ後の画面(iOS)

7. レンダラーの実装(Android)

 Androidでのロングタップの検出は、iOSと比べるとやや複雑になっている。具体的には、リスナークラスを定義して、これをジャスチャーディテクター経由でコントロール上のイベントにひも付ける作業になる。

 それでは、順に手順を見ていこう。

 リスナークラスは、GestureSample.Droidプロジェクトに、MyGestureListener.csファイルを追加し、以下のように実装する。

C#
using Android.Views;

namespace GestureSample.Droid {
  internal class MyGestureListener : GestureDetector.SimpleOnGestureListener { //1

    public ExImage ExImage { private get; set; } // ← 2

    public override void OnLongPress(MotionEvent e) { // ← 3
      base.OnLongPress(e);
      if (ExImage != null) {
        ExImage.OnLongPress(); // ← 4
      }
    }
  }
}
リスナークラスのコード(MyGestureListener.cs)

 リスナークラスは、GestureDetector.SimpleOnGestureListenerクラスを継承して作成する(1)。

 リスナークラスのExImageプロパティは、外から設定可能な拡張イメージへのポインターである(2)。

 GestureDetector.SimpleOnGestureListenerクラスは、各種のジェスチャーをハンドルするOnで始まるメソッドを持っており、今回は、ロングタップ時に呼ばれるOnLongPressメソッドをオーバーライドして(3)、そこからExImageオブジェクトのOnLongPressメソッドを呼び出した(4)。

 続いて、レンダラークラスを定義する。GestureSample.Droidプロジェクトに、ExImageRenderer.csファイルを追加し、以下のように実装する。

C#
using Android.Views;
using GestureSample;
using GestureSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(ExImage), typeof(ExImageRenderer))]
namespace GestureSample.Droid {
  internal class ExImageRenderer : ImageRenderer {
    private readonly MyGestureListener _listener; // ← 1
    private readonly GestureDetector _detector; // ← 2

    public ExImageRenderer() { // ←3
      _listener = new MyGestureListener();
      _detector = new GestureDetector(_listener);
    }

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

      _listener.ExImage = Element as ExImage; // ← 4

      GenericMotion += (s, a) => _detector.OnTouchEvent(a.Event); // ← 5
      Touch += (s, a) => _detector.OnTouchEvent(a.Event); // ← 6

    }
  }
}
Android用のレンダラー実装コード(ExImageRenderer.cs)

 最初に、先ほど作成したMyGestureListenerクラス(1)と、これを関連付けるために使用するGestureDetectorクラス(2)を定義し、レンダラークラス(ExImageRendererクラス)のコンストラクターで、これらのインスタンスを生成する(3)。

 リスナークラスのExImageプロパティは、OnElementChangedメソッド内で初期化される(4)。

 ひも付けは、GenericMotionイベントとTouchイベントに対して行う(56)。実は、ロングタップだけなら、Touchイベントだけでよいのだが、今後の拡張のために、ここでは両方のイベントに同じようにひも付けを行っておく。

 その他、レンダラーとしての実装は、iOSのものと同じである。

 このコードを実行して、Imageビューをロングタップすると、次のような画面になる。

図7 ロングタップ後の画面(Android)
図7 ロングタップ後の画面(Android)

8. その他のジェスチャー

 今回、iOSで使用した、UILongPressGestureRecognizerクラスは、ロングプレスを検出するためのものであるが、これを、

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

などのクラスに置き換えれば、それぞれのジャスチャーを処理できる。

 またAndroidでは、リスナークラスでOnLongPressメソッドをオーバーライドしたが、GestureDetector.SimpleOnGestureListenerでは、この他にも、

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

などのメソッドが、オーバーライド可能である。

 これらを使用することで、各プラットフォームで処理できるイベントは、全てXamarin.Formsでも利用可能となる。

9. まとめ

 今回は、Xamarin.Formsでタップおよびロングタップを処理する方法を紹介した。また、レンダラーを実装することで、その他のジェスチャーも全て処理可能であることを示した。

 しかし、各プラットフォームでのジャスチャーなどの扱いは、決して共通化されているわけではない。そのため、Xamarin.Formsから全てを同じように扱うとなると、いろいろと解決すべき問題が残る。この辺が、Xamarin.Formsで、現在タップしか扱われていない理由なのかもしれない。

※以下では、本稿の前後を合わせて5回分(第33回~第37回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Xamarin逆引きTips
33. Xamarin Studio/Visual Studioで「Ricty Diminished」プログラミング用フォントを使うには?

「Ricty Diminished」や「Source Code Pro」などのプログラミング用フォントを、Xamarin Studio/Visual Studioのコードエディターのフォントとして設定する方法。

Xamarin逆引きTips
34. Xamarin.FormsでBoxViewコントロールを拡張するには?

四角形を描画するBoxViewコントロールを拡張してネイティブ側で描画することで、角丸・枠線・影付きなどを実現する方法を説明する。

Xamarin逆引きTips
35. 【現在、表示中】≫ Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)

iOS/Androidにおけるタップやスワイプなどの各種ジェスチャーを、Xamarin.Formsで処理する方法を解説する。

Xamarin逆引きTips
36. Xamarin.Formsでツールバーアイテムによるメニューを設置するには?

PageクラスのToolbarItemsプロパティを使って、画面の上部にツールバー(Android)/ナビゲーションバー(iOS)を表示する方法を解説する。

Xamarin逆引きTips
37. MvvmCrossのプロジェクトをセットアップするには?

クロスプラットフォーム開発を支援するXamarin用ライブラリの「MvvmCross」を使ってiOS/Androidアプリ開発を行うためのプロジェクトの作成方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!