Xamarin.AndroidはViewのClickリスナーを複数登録できる話

このエントリーをはてなブックマークに追加

Androidアプリ作ってる人にView.setOnClickListenerを使ったことがない人はいないかと思います。 今日はそんなsetOnClickListenerな話です。

※ViewのクリックをハンドリングすることをClickリスナーと表現しておきます。(AndroidとXamarin.Androidで少し名前違うので)

Android APIのClickリスナーは1つしか登録できない

setOnClickListenerの名前から分かりますが、setOnClickListenerメソッドで登録できるOnClickListenerは1インスタンスしか登録できません。 そう、addとかできないんです。

と、言っても名前詐欺の可能性も否定できないのでViewクラスのソースを見ましょう。

View.setOnClickListenerのソースはgooglesourceを頑張ったら見れます。Githubではファイルサイズが大きすぎてRaw表示のみでした。 そこで肝心のソースですが、以下のようになっています。

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

getListnerInfo()で返却されるオブジェクトのフィールドにリスナーをsetしていることがわかります。(Javaにはプロパティがないのでフィールドに確定されます) そのため、getListenerInfo()のインスタンスが1つに収束しているならば、リスナーは1つしか設定できない機構であることが証明できます。

そこで肝心のView.getListenerInfoのソースですが、以下のようになっています。

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

フィールドを遅延初期化するだけのメソッドのようです。getListenerInfo()のインスタンスが1つに収束してるので、リスナーは1つしか設定できない機構であることは証明できました。(すごい当たり前ですが)

Android APIでClickリスナーを複数登録するには

もし、Androidで複数のClickリスナーを登録しようと思ったらなんらかのカスタムクラスを作る必要が出てきます。またはリフレクションで頑張るか。

なぜそうなるかというと、Clickリスナー系のメソッドはsetとhasしか用意されていないからです。getがないんですよ……

しかも、hasのメソッド名はhasOnClickListenersなんですよね。 気づきましたか?hasOnClickListenersで複数形です。setできるのは単数なのに。

というわけで、Clickリスナーを集約するリスナーを用意するにはカスタムクラスかリフレクションで頑張るしかないのです。

Xamarin.Android APIのClickリスナーはeventなので複数登録できる

Xamarin.AndroidではAndroidのイベント系リスナーをC#のeventとして用意されていたりします。 もちろんAndroidのView.setOnClickListenerもevent化されていて、View.Clickにされています。(いちおうView.SetOnClickListenerも用意されているみたいですが使うことはあまりないかなと)

eventなのでaddとremoveができるというわけです。

Clickリスナーの登録方法はC#のevent構文そのままで、サンプルコードを用意するとこんな感じになります。

        private bool isAddEventHandler = false;
        private TextView textView;

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

            textView = new TextView(this)
            {
                Gravity = GravityFlags.Center,
                Text = "Hello, Text"
            };
            SetContentView(textView);

            textView.Click += (s, e) => textView.Text += "a";
            textView.Click += (s, e) => textView.Text += "b";
            textView.Click += (s, e) =>
            {
                if (isAddEventHandler)
                {
                    textView.Click -= textClicked;
                    isAddEventHandler = false;
                }
                else
                {
                    textView.Click += textClicked;
                    isAddEventHandler = true;
                }
            };
        }

        private void textClicked(object sender, EventArgs args)
        {
            textView.Text += "c";
        }

このコードなんだと思いますか?

答えは本当にadd/removeが機能してるか確認したコードです。 動作結果はちゃんとevent実装されてました。 (こんなことしないと確認できないぐらいXamarin.Androidの中身が追いにくいのつらい)

どうしてevent化できてるのか私にはよくわからないですが、Xamarin.AndroidがAndroid APIをラップするときに頑張ったのでしょう(たぶん)

おそらく変換するときにクラスの拡張のようなものでClickリスナーを集約するリスナーでも用意したのかと思いますが… (誰かこのあたりのソースあったら教えて)

Xamarin.Android APIでhasOnClickListenersはどうなってんの

当然生まれてくる疑問です。Xamarin.AndroidではView.HasOnClickListenersとして定義され、自分で検証した限りでは正確な実装ができているようです。 View.Clickに1つでもデリゲートが登録されてるとtrueとなり、すべてのデリゲートが解除されたらfalseとなっていました。

と、いいつつこのあたり気力がなくなってあいまいな検証にはなってしまいました。View.SetOnClickListenerを使ったらどうなるとか、そもそもそれ使えるの?とかは未検証です。

そういうわけでイベントリスナー系に関してはAndroid APIよりXamarin.Android APIのほうが優秀と言っていいかなーと思います。(そしてその謎対応がある分、闇はありそうです)