Xamarin.Forms でネイティブのイベントハンドラを拾う(iOS/Android編)

Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/5908

の続きです。やり方は同じなのですが、iOS/Androidの場合にはネイティブのコントロールには Name プロパティがないので、Windows Phone のような FindName メソッドはないですね。iOS の場合は Outlet、Android の場合は FindViewById になるので、操作がちょっと違います。このあたりは、似たような操作(あるいは、適当なメソッドで包んでしまう)にしないと、手間がかかるので後でまとめていきましょう。

■iOSのRendererを表示する。

iOS の場合は、画面のルートが UIViewController で、その下の各種の UIView があります。Xamarin製XAMLでPageオブジェクトを作成した後は、CreateViewControllerメソッドでUIViewControllerを作ります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    return true;
}

void Disp(UIViewController vc, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vc.GetType().Name);
    Disp(vc.View);
}
void Disp(UIView vi, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vi.GetType().Name);
    foreach (var it in vi.Subviews)
    {
        Disp(it, spc + " ");
    }
}

■iOSのネイティブコントロールにイベントをつける

方法は Windows Phone と同じで、レンダラのツリーを探索してネイティブのコントロールを探し出します。Xamarin製XAMLには名前をつけておいて、対応するネイティブコントロールを返す SetNamePageToUIelement メソッドを作ります。

UIControl SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // var en = rend as EntryRenderer;
            // en.Control.Name = name;
            // リフレクションで
            var pa = rend as UIView;
            var pi = pa.GetType().GetProperty("Control");
            var obj = pi.GetValue(pa);
            //obj.GetType().GetProperty("Name").SetValue(obj, name);

            return obj as UIControl;
        }
    }
    return null;
}

UIView FindRenderer(View ent)
{
    return Search(window.RootViewController.View, ent);
}
UIView Search(UIView el, View ent)
{

    var pa = el as UIView;
    if (pa != null)
    {
        var pi = pa.GetType().GetProperty("Element");
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        foreach (var it in pa.Subviews)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

レンダラのツリーでペアになっている、Xamarin製コントロールとiOS謹製コントロールは、それぞれ Element プロパティと Control プロパティで取得できます。ただし、レンダラーで使ってるクラスが ViewRenderer<TView, TNativeView> のようにジェネリックになっているため、各コントロールごとにクラスが作成されています。対応するコントロールに対してのキャストをいちいちやってもいいのですが、所詮プロパティだけが欲しいのですから、リフレクションを使って省力化します。

ネイティブコントロールは UIControl を基底クラスにしているので、これを戻します。各種のイベントを付加したい場合は、もとのクラスにキャストする必要あります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    UIControl uc = SetNamePageToUIelement("textUserName", page);
    var obj = uc as UITextField;
    obj.AllTouchEvents += obj_AllTouchEvents;

    return true;
}

■AndroidでRendererを表示する

Androidの場合は明示的なコンバーターを呼び出していません。SetPage メソッド内で隠蔽化されていて、レンダラのルートが解りづらいのですが、this.Window.DecorView でルートとなるビューが取得できます。ややこしいのですが、Android.Views.View と Xamarin.Forms.View と名前が混在しています。Xamarin.Forms.View のほうは、Xamarin製XAMLで使うViewで、Android.Views.View のほうはレンダリングツリーの構築のための View です。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);
}

void Disp(Android.Views.View vi, string spc = "")
{
    System.Diagnostics.Debug.WriteLine("{0}{1}", spc, vi.GetType().Name);
    var vg = vi as ViewGroup;
    if (vg != null)
    {
        for (int i = 0; i < vg.ChildCount; i++)
        {
            var v = vg.GetChildAt(i);
            Disp(v, spc + " ");
        }
    }
}

Android の場合、子コントロールを取得するためには ViewGroup にキャストをします。Children コレクションを持たせてもいいような気がするのですが、Android はそういう流儀みたいです。

■Androidのネイティブコントロールにイベントを設定する

戻り値のオブジェクトが Android.Views.View になるだけで、iOS と動作は同じです。このあたりは、Windows Phone も含めてライブラリ化したいところですね。

Android.Views.View SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<Xamarin.Forms.View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // リフレクションで
            var pa = rend as Android.Views.View;
            var pi = pa.GetType().GetProperty("Control");
            var obj = pi.GetValue(pa);

            return obj as Android.Views.View;
        }
    }
    return null;
}

Android.Views.View FindRenderer(Xamarin.Forms.View ent)
{
    return Search(this.Window.DecorView, ent);
}
Android.Views.View Search(Android.Views.View el, Xamarin.Forms.View ent)
{

    var pa = el as Android.Views.View;
    if (pa != null)
    {

        var pi = pa.GetType().GetProperty("Element");
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        var vg = pa as ViewGroup;
        if (vg != null)
        {
            for (int i = 0; i < vg.ChildCount; i++)
            {
                var ret = Search(vg.GetChildAt(i), ent);
                if (ret != null)
                {
                    return ret;
                }
            }
        }
    }
    return null;
}

イベントの種類が、iOS/Android/WP と随分違うので一概に共通化できませんが、それぞれのネイティブのイベントを使うことができます。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);

    Android.Views.View vi = SetNamePageToUIelement("textUserName", page);
    vi.FocusChange += vi_FocusChange;
}

おそらく、将来的には基本的なタップイベントのようなものは、Xamarin.Forms で実装されるでしょうから、こまめに共通化してもあまり意味はないかなと思っています(まあ、現時点では Click イベントぐらいしかないので、実務的な意味あるんですが)。むしろ、スワイプやピンチのような特有な操作を共通にしておくとライブラリ的に意味があるかもしれません。ちょっとそのあたりは Xamarin.Forms のイベント絡みがどうなるのかが不明なので、なんとも言えませんね。

ただ、自前の TMPuzzle を移植してみた感じでは、圧倒的にイベント絡みの処理は足りなそうなので、なんらかの補完はしないと駄目そうです。ゲームアプリの場合には、ピンチ、スワイプ、コマのスライド、得点のアニメーションなど、通常のコントロールにはない操作が出てくるので、そのあたりが必要です。ええ、もちろん Unity や MonoGame を使えばいいんでしょうが、手軽に作れるパズルアプリってのは考えているので、そのあたりはおいおいと。

カテゴリー: 開発, Android, Xamarin, iOS パーマリンク