itemRenderer パート4 : ステート & トランジション

原文 : http://weblogs.macromedia.com/pent/archives/2008/03/itemrenderers_p_3.html

itemRenderers: Part 4: ステート と トランジション

itemRenderer は、視覚的に情報を伝えることにとても優れています。

伝えると一口で言っても、名前を表示するだけのシンプルなものから、たくさんの色を使い表現するもの、時にはインタラクションを含むものまであります。

itemEditor がまさにインタラクティブなコントロールですが、今回の記事の目的ではありませんので割愛します。
今回の記事では、itemRenderer の data プロパティやユーザのアクションにより外観を変化させる itemRenderer について見ていきます。

■ ステート

<mx:State> は、itemRendererの外観を変更するのにとても適した方法です。
ステートはとても簡単に使うことができ、トランジションと組み合わせることにより、ユーザにフィードバックを返して、使い勝手をよくすることができます。

今回のサンプルでは、List コンポーネント用に、MXML を用いた itemRenderer を作成していきます。( ActionScript のみでも作成可能です )

itemRenderer で表示するのは、image, title, author, price と 本を購入するための Button コントロールです。

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" >

<mx:Image id="bookImage" source="{data.image}" />
<mx:VBox height="115" width="100%" verticalAlign="top" verticalGap="0" paddingRight="10">
<mx:Text text="{data.title}" fontWeight="bold" width="100%"/>
<mx:Label text="{data.author}" />
<mx:HBox id="priceBox" width="100%">
<mx:Label text="{data.price}" width="100%"/>
<mx:Button label="Buy" />
</mx:HBox>
</mx:VBox>
</mx:HBox>

もし本の在庫が無ければ、その本が非表示になるようにしましょう( data の <instock> タグの値 ( yes / no ) で在庫の有無を判定します )。
実装を簡単にするために、HBox の id に “priceBox” を指定しています。
この HBox の可視状態を変更することにより、内部の Label と Button 両方の可視状態も変更できるからです。

set data メソッドをオーバーライドしてもいいのですが、ここでは priceBox の可視状態を直接変更せずに、ステートを使ってみます。

<mx:states>
<mx:State name="NoStockState">
<mx:SetProperty target="{priceBox}" name="visible" value="false"/>
</mx:State>
</mx:states>

このタグをルートタグ直下に配置してください。

やりたいことが簡単なわりに少々複雑になってしまっていますが、ステートの使い方がよくわかると思います。
以下の2つのステートが存在します。

・ ベースステート : これはコンポーネントの標準(デフォルト)の状態です。コンポーネントがステートを使っていないときは、この状態になっています。今回の例では、ステートがベースステートの時には priceBox の visible プロパティが true (デフォルト値) になります。 これは instock タグの値が yes の時です。

・ NoStockState : これは instock タグの値が no の時のステートです。 このステートになると <mx:State> タグ内の SetProperty が処理されます。 target はそのSetProperty 処理の対象となるインスタンスを指定します。name プロパティはアップデートするプロパティ名、value プロパティは新しくセットする値です。

set data メソッドは、instock の値に基づいてステートを切り替えます。

override public function set data( value:Object ) : void
{
super.data = value;

if( data )
{
if( data.instock == "yes" )
currentState = "";
else
currentState = "NoStockState";
}
}

currentState は全ての UIComponent コントロールが備えているプロパティであり、現在のステート名を保持しています。 ステートを切り替えると、Flex フレームワークはベースステートから始め、新たなステートへの切り替えに必要な処理を行います。

—————-
itemRenderer は再利用されますので、必ず元の値(状態)に戻す処理を入れてください。itemRenderer に if を書いたら、必ず else も書く必要があります。
—————-

もし興味があればですが、下のコードのように set data メソッドをオーバーライドしない方法もあります。 データバインディングを使って、 root タグの中で currentState の値を直接変更しています。

<mx:HBox xmlns:mx=”http://www.adobe.com/2006/mxml” width=”400″
currentState=”{data.instock == ‘yes’ ? ” : ‘NoStockState’}” >

インラインで data.instock の値を評価し、その結果を currentState プロパティに代入しています。 トリッキーなので保守性には優れていないかもしれません。

■ エレメントの追加

次の itemRenderer では、instack の値が yes の時にだけ price と 購入ボタンが表示されます。
もちろんステートを使わずに同じことは出来ますが、もし itemRenderer に追加、削除するコントロールが多数ある場合にはステートを使うのが得策です。 
なぜなら、itemRenderer の currentState プロパティに値をセットするだけでそれら多数のコントロールを管理できるからです。

単に price と 購入ボタン を削除するだけでなく、在庫切れであることを表示する Label も追加します。

変更を加えたコードは次のようになります。

<mx:states>
<mx:State name="NoStockState">
<mx:SetProperty target="{priceBox}" name="visible" value="false"/>
<mx:AddChild relativeTo="{priceBox}" position="before">
<mx:Label text="-- currently not in stock --" color="#73DAF0"/>
</mx:AddChild>
</mx:State>
</mx:states>

<mx:AddChild> タグで priceBox に Label を追加しています。
priceBox の visible プロパティに false をセットし、代わりにわかりやすい文字列を表示します。

繰り返しますが、set data メソッドをオーバーライドすることで、同じように Label を追加することもできます。また、必要なコンポーネントを前もって追加しておいて、それらの可視状態を後で変更するといった方法も可能です。

しかし、ステートにはわかりやすいメリットがあります。
ステートを使えば、在庫切れ状態の表示に必要な処理がどんなに複雑になっても、NoStockState を編集するだけで十分です。ステートを切り替えるための ActionScript コードを変更する必要はありません。

—————
Flex Builder の Design View ではステートを編集することができます。
—————

■ 伸び縮みする List

今回の例は、List コントロールではうまく動作しませんが、VBox と Repeater を用いて実現できます。
もし List コントロールを使用して、そのリストがスクロールされてしまうと伸長させたアイテムの itemRenderer が機能しなくなるかもしれません。
例えば、高さが同じアイテムを複数保持するリストがあるとします。
ここで2番目のアイテムの高さを伸長させると、そのアイテムの高さは他のアイテムよりも高くなります。 ここまでは大丈夫です。
( And there’s the catch: the visible items. )
次にそのリストをスクロールさせます。 itemRenderer は再利用されるのを覚えていますよね。 2番目のアイテムがリストの外に出ると、そのアイテムの itemRenderer はリストの最下部のアイテムを表示するために移動します。
その時、その itemRenderer の高さを元に戻す必要があります。
そして、2番目のアイテムが再度見えるようにListをスクロールします。
2番目のアイテムの高さが伸長されたままになっていて欲しいところですが、itemRenderer は前の高さのことを知りません。
以前の記事に書いたように、それらの情報は itemRenderer の data プロパティ、もしくは何かしらの外部ソースから得なければなりません。

itemRenderer のリサイズはとても複雑になりがちなので、頑張って実装するわりには大したものにならないと思います。
もしリサイズが必要なのであれば VBox と Repeater を用いる方法が良いと思います。しかし Repeater を用いる方法の欠点として、Repeater で表示する子コンポーネントのインスタンス全てが生成されてしまいます。
もし Repeater を用いて 1000 個のレコードを表示すると、itemRenderer のインスタンスが1000 個生成されてしまいます。

今回の例では、VBox の 子コンポーネントとなる itemRenderer を作成します。
本のタイトルと著者のみを表示するとてもシンプルな itemRenderer ですが、クリックするとその場で itemRenderer が伸長するようにします。
これは下記の2つを使って実現できます。

* itemRenderer に追加情報を持つステートを持たせる
* itemRenderer をスムーズに伸縮させるため Resize トランジションを使用する

itemRenerer のベース ステートはとてもシンプルです:

<mx:HBox width="100%">
<mx:Label text="{data.author}" fontWeight="bold"/>
<mx:Text text="{data.title}" width="100%" fontSize="12" selectable="false"/>
</mx:HBox>

ExpandedState ステートでは新たな情報を付加して itemRenderer の高さの調節をします。

<mx:states>
<mx:State name="ExpandedState">
<mx:AddChild position="lastChild">
<mx:HBox width="100%">
<mx:Image source="{data.image}"/>
<mx:Spacer width="100%"/>
<mx:Label text="{data.price}"/>
<mx:Button label="Buy"/>
</mx:HBox>
</mx:AddChild>
</mx:State>
</mx:states>

itemRenderer のサイズ変更にかかる手間は、トランジションをただ追加するのと大差ありません。

<mx:transitions>
<mx:Transition fromState="*" toState="*">
<mx:Resize target="{this}" />
</mx:Transition>
</mx:transitions>

この トランジションは fromState と toState プロパティの両方にワイルドカードが指定されているので、ステートが変更される毎に再生されます。

後は、itemRenderer のクリックイベントのハンドラを用意し、そのハンドラ内でステートを変更するだけです。

<mx:Script>
<![CDATA[

private function expandItem() : void
{
if( currentState == "ExpandedState" )
currentState = "";
else
currentState = "ExpandedState";
}
]]>
</mx:Script>

■ Summary

ステートは、itemRenderer の外観を変更するのにとても良い方法です。
複数の変更をステートとしてまとめることにより、itemRendererへの変更を一斉に反映させることができます。

次回の記事では、UIComponent クラスを継承して効率的な itemRenderer を作成します。