【20日目】Titanium 3.0 のイベント伝播を見てみよう

この記事は @astronaughts (通称・あすとろなんとか) さん主催の Titanium Mobile Advent Calendar 2012 向けに書いています。

さて、Strict Mode といい、 Titanium 3.0 の細かくも大事な大事な変更点に UI Event Bubbling があります。これまで「なんで Titanium の addEventListener メソッドは第3引数 (Use Capture) が無いんだろう」とか「なんで Titanium には stopPropagation が無いんだろう」とか考えていたのですが、ついにイベントの伝播制御ができるようになりました。

@ このプロパティ、結構重要な追加点だと思うのですが全然話題に上がらない。。。寂しい(昨日の引きずってる)
@k0sukey
Kosuke Isobe

@k0sukey さんがこのように仰るので、ちょっと調べてみましょう。

イベント

Titanium とイベントは切っても切り離せない関係です。イベントを登録しておいて、必要に応じて発火させることで様々な処理を「非同期で」行うことが可能です。非同期だからこそ、とある処理が終わるのを待たなくても次の処理を開始することができたり、とある処理の結果を使って何かをやりたいのに、結果が undefined で混乱することになるわけです。

グローバルイベント

Titanium は2種類のイベントを持ちます。1つはグローバルイベントです。 Ti.App の下には addEventListener / fireEvent メソッドがあり、これらを使うことでアプリケーション内でグローバルに参照し、発火させられるイベントを登録することが可能です。

addEventListener には同じ名前でいくつもイベントを登録できるので、 fireEvent を使うと同じ名前で登録したイベントは全て発火します。ループ中でグローバルなイベントリスナを設置する場合は注意しましょう。 removeEventListener を使って上手にイベント管理をしてください。

自由にイベントのやり取りができるので便利ですが、きちんと管理しないと混乱の元にもなる諸刃の剣です。

UI イベント

もう1つが UI イベントです。名前の通り UI コンポーネントに持たせるイベントで、 Ti.UI.foobar オブジェクトに addEventListener を使ってイベントを持たせる形です。分かりやすいところでは、

のように、 UI オブジェクトに対して予め用意されているイベントハンドラ (この例では click ) と、呼び出したい関数名または匿名関数を登録することで指定した動作でイベントを発火できるというものです。

UI オブジェクトのイベントハンドラは click や dblclick 、 swipe など、人の操作に応じた分かりやすいものが用意されていますが、グローバルオブジェクトと同様に自分だけのオリジナルハンドラを用意することも可能です。

このように afterclick のような非標準のイベントハンドラを用意しておいて、

のように fireEvent メソッドでイベントの発火が可能です。 UI コンポーネントの表示制御をイベントで行う場合にカスタムイベントは便利に使うことができます。

これまでの問題

実は Titanium 2.x まではイベントの「伝播」に関する制御が公式に実装されていませんでした。イベントの伝播とは何でしょうか ? 簡単な例で紹介します。

Window の上に View 1つ、 Button 3つを乗せただけのアプリです。これら全てに click イベントを貼っています。ポチッとしたらイベントが発火するわけですね。では、早速実行して、そうですね、 Button 3 を押してみましょう。

what-is-this

おや ? 確かに Button 3 を押したのに View と Window の click イベントまで発火されています。これがイベントの伝播です。 UI イベントは親要素が同じイベントハンドラを持っている場合、どんどん伝播していきます。この例では

  • ボタン
  • 親 : View
  • 祖 : Window

という関係ですね ? これらに click イベントハンドラを登録しておくと最後の最後までイベントが伝播していくわけです。

予め用意されているイベントハンドラだけでなく、オリジナルのイベントハンドラでも同様です。 Button 1 と Window に originalevent というイベントハンドラを設定し、 Button 3 の click イベント内で Button 1 の originalevent イベントを発火してみます。

さて、実行してみるとどうなるでしょうか ?

what-is-this2

奇妙なことになりました。 Button 3 を押すことで Button 1 の originalevent が発火し、 Button 3 の click イベントが伝播して View と Window の両方でも発火し、さらには Button 1 の originalevent が伝播して Window でも発火しています。

ただただポチッとしたコンポーネントだけでイベント処理ができれば良いのに、これでは面倒くさいですよね。例えば TableViewRow の上に乗せた Label を click したら TableViewRow の click イベントまで発火しちゃったなんて良く聞く話です。

本題 : イベントの伝播を制御する

さあ本題です。 Titanium 3.0 ではイベントの伝播制御ができるようになりました。「ここでイベントの伝播終わり ! 」とか「このコンポーネントはイベント伝播させないから」みたいな処理が簡単にかけるようになったのです。

各 Button のプロパティに bubbleParent: false を追加しました。これは Titanium 3.0 から使えるようになった UI コンポーネント用の新しいプロパティです。早速実行してみます。

non-bubbling

Button 3 をクリックして Button 1 の originalevent を発火させても Window の originalevent は発火していません。同様に Button 2 をクリックしても View と Window のイベントは発火しませんし、 Button 1 も同じですね。

UI イベントの場合はとてもシンプルです。 bubbleParent: false を追加するだけでイベントの伝播が無くなります。では、グローバルイベントやオリジナルのイベントハンドラではどうでしょうか ? コードを修正してみましょう。

Button 3 の bubbleParent を true にします。これでイベントの伝播を許可させます。さらに Button 3 の click イベントハンドラ内では Button 1 の originalevent を発火させていますが、引き渡す情報の中に cancelBubble: true を入れています。

non-bubbling2

いざ実行してみると、Button 3 の click イベントは確かに伝播していますが、 発火された Button 1 の originalevent が Window に伝播していません。 fireEvent メソッドを使うときは引き渡す情報に calcelBubble: true をセットすることで親要素の同名イベント発火を止めることができています。

ややこしいのは、 UI コンポーネントからイベント伝播を止めるときは bubbleParent の値を false にして、 fireEvent から止めるときは cancelBubble を true にするというところでしょうか。 UI コンポーネント側でも cancelBubble の true / false で制御させてくれたら良いのにと思います(´・ω・`)

Titanium 3.0 でイベント伝播制御の手段を手にすることになりました。これだけでも Titanium 3.0 を使う価値があるんじゃないなと個人的には思います。 TableViewRow の上に ScrollView 乗せるなんて、これまでは鬼門だったのですが (Scroll イベント発火しまくりです) 、 Titanium 3.0 では何とかなるんじゃないかと思います。まだ未検証ですが。

これで20日目は終わりです。明日21日目は @ww24 さんです ! CODESTRONG !

追記

@ 書こうかなと思った内容を書かれた。一点付け加えるとUIイベントのコールバックの引数(e)に対してe.cancelBubble=trueにするとそれ以上親には上がって行きません。イベントの種類によって伝播する範囲を分けたい場合に使えます。 #titanaiumjp

ありがとうございます!