itemRenderer

2008年08月27日

   itemRenderer パート4 : ステート & トランジション をはてなブックマークに追加 itemRenderer パート4 : ステート & トランジション のはてなブックマーク数 この記事をクリップ! Yahoo!ブックマークに登録

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 を作成します。




banana_systems at 12:26コメント(0)トラックバック(0)  この記事をクリップ!

2008年07月29日

   itemRenderer パート3 : Communication ( データのやりとり ) をはてなブックマークに追加 itemRenderer パート3 : Communication ( データのやりとり ) のはてなブックマーク数 この記事をクリップ! Yahoo!ブックマークに登録

itemRenderer パート3 : Communication ( データのやりとり )

○ 原文
itemRenderers: Part 3: Communication

前回の記事では external itemRenderer の作成方法を、MXML と ActionScript の両方で説明しました。
サンプルコードでは、itemRenderer 内の Button がクリックされるとカスタム・イベント(BuyBookEvent)をディスパッチして、itemRenderer外でそのイベントに応じた処理を行えるようにしました。
今回の記事では、itemRenderer とのデータのやり取りを掘り下げていきたいと思います。

絶対に破ってほしくないルールが一つあります。それは「(外部から) itemRenderer のインスタンスを保持し、(publicなプロパティをセットすることにより)itemRendererを変更したり、itemRenderer の public メソッドを呼び出してはいけない」ということです。
Part 1 の記事で述べたように、 itemRenderer は再利用されるものなので保持するのはとても難しく、もしそれを行ってしまうと Flex frameworkの挙動を狂わせかねません。

このルールをに則って考えると、itemRenderer でできるのは次のようなことです。

* itemRenderer は自身を適用したコンポーネントを通じてイベントをディスパッチできます。(既にバブリングを紹介しました。この方法が優れていることを後で説明します)
* itemRenderer では Application.application などのクラス変数が使用可能です。Application オブジェクトに「グローバルに」変数を定義したのなら、この方法でその変数にアクセス可能です。
* itemRenderer は自身を適用したコンポーネントのpublic 変数にアクセス可能です。
* itemRenderer は data のレコードの全てのフィールドにアクセスできます。例えば、直接画面に表示するデータでなくとも、itemRendererの動作に影響を与えるフィールドにアクセスしたりできます。


■ itemRenderer の処理を動的に変更する

次のコードは、前回の記事で TileList 用に作成した MXML の itemRenderer です。
これを外部のデータによってitemRendererの処理を動的に変えるようにします。 ( ファイル名を BookItemRenderer.mxml とします )
<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="115" >

<mx:Script>
<![CDATA[
]]>
</mx:Script>

<mx:Image id="bookImage" source="{data.image}" />
<mx:VBox height="115" verticalAlign="top" verticalGap="0">
<mx:Text text="{data.title}" fontWeight="bold" width="100%"/>
<mx:Spacer height="20" />
<mx:Label text="{data.author}" />
<mx:Label text="Available {data.date}" />
<mx:Spacer height="100%" />
<mx:HBox width="100%" horizontalAlign="right">
<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]">
<mx:click>
<![CDATA[
var e:BuyBookEvent = new BuyBookEvent();
e.bookData = data;
dispatchEvent(e);
]]>
</mx:click>
</mx:Button>
</mx:HBox>
</mx:VBox>

</mx:HBox>


TileList を用いてアイテムのカタログの表示をしようとしているとします。
金額の範囲を設定できる Slider コントロールも存在するとしましょう(Slider コントロールは itemRenderer の外側にあります)。
金額の範囲外のアイテムは、フェードアウトさせます(itemRendererのアルファ値を変化させます)。
itemRenderer の alpha 値を変更するためには、金額の範囲が変わったことを全ての itemRenderer に伝える必要があります。

set data メソッドを次のようにオーバーライドします。

override public function set data( value:Object ) : void
{
super.data = value;
if( data.price < criteria ) alpha = 0.4;
else alpha = 1;
}


問題は、どのように criteria (金額の範囲)の値を変えればいいかです。
itemRenderer を使う場合のベスト・プラクティスは、「itemRenderer は常に与えられたデータのみに基づいて動作させる」ことです。
しかし、今回のようなケースでは criteria を data に含めるのは良い方法とは言えませんので、data の外に存在するようにしましょう。
これを実現する方法はいくつかあります。

* list に含める。 list ( List, DataGrid, TileList 等 ) コンポーネントを継承し、その継承したクラスに public な変数として criteria (金額の範囲) を保持させる。
* グローバル変数として application オブジェクトに含める。

私ならば、一番目のクラスを継承して criteria をクラスに含める方法を選びます。
つまるところ、クラスはデータを表示するために使われていて、criteria は表示される項目の一部なのですから。
今回の例では、TileListを継承して criteria を public なデータメンバーとして持たせます。

package
{

import mx.controls.TileList;

public class CatalogList extends TileList
{
public function CatalogList()
{
super();
}

private var _criteria:Number = 10;

public function get critera() : Number
{
return _criteria;
}

public function set criteria( value:Number ) : void
{
_criteria = value;
}
}
}


itemRenderer の外に存在するコントロール (今回の例では、Sliderコントロール) がこのクラスの criteria プロパティに値をセットすることで、itemRenderer に criteria の値を通知できるようになりました。

■ listData

itemRenderer は、itemRenderer がセットされたリスト自身の情報と、itemRenderer がどの行と列(DataGrid のように列を持つコンポーネントの時のみ)描画しているかを知ることができます。
この情報が listData です。listData を使うと、次のように BookItemRenderer.mxml を書き直すことができます。

override public function set data( value:Object ) : void
{
super.data = value;
var criteria:Number = (listData.owner as MyTileList).criteria;
if( data.price < criteria ) alpha = 0.4;
else alpha = 1;
}


先に見せた BooktItemRenderer.mxml の <mx:Script> タグ内にこのメソッドを追加してみてください。

listData プロパティは itemRenderer が属するコントロールへの参照である owner プロパティを持っています。
今回のサンプルでは、owner は TileList を継承した MyTitleList です。 owener プロパティを MyTitleList へキャストすることにより、criteria へアクセス可能になります。

■ IDropInListItemRenderer

listData は、IDropInListItemRenderer インターフェイスを実装している itemRenderer にのみ存在します。
残念ながら、コンテナ(HBoxなど) はそのインターフェイスを実装していません。
コントロール (ButtonやLabelなど) はそのインターフェイスを実装していますが、コンテナの場合は自分でそのインターフェイスを実装しなければなりません。

このインターフェイスの実装方法は簡単で、Flexのドキュメントにも記述されています。
IDropInListItemRenderer を実装した BookItemRenderer クラスを自分で作るには、次のようにします。

1. IDropInListItemRenderer インターフェイスを実装したクラスを用意する

<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" ... implements="mx.controls.listClasses.IDropInListItemRenderer">


2. listData の set と get メソッドを itemRenderer となるクラスの <mx:Script> タグ内に追加する。

import mx.controls.listClasses.BaseListData;

private var _listData:BaseListData;
public function get listData() : BaseListData
{
return _listData;
}
public function set listData( value:BaseListData ) : void
{
_listData = value;
}


itemRenderer が IDropInListItemRenderer インターフェイスを実装していれば、list コンポーネントは listData を全ての itemRenderer にセットします。

■ invalidateList()

criteriaをクラスに(正常に機能するものとして)含めるのは少々複雑で、単にcriteriaに値を代入しただけではFlex frameworkはその値の変化に気付いてくれません。criteriaの値の変更時には、その変更を伝えるイベントのトリガーが必要です。
次のコードは、set criteria メソッドを変更したものです。

public function set criteria( value:Number ) : void
{
_criteria = value;

invalidateList();
}


_criteria へ値をセットした後にinvalidateList()を呼び出しています。
invalidateList()を呼び出すことにより、全てのitemRenderer に dataProvider の値でデータをリセットさせます。リセットすることにより set data メソッドが再度呼ばれます。
これらの処理は次のように表現できます。

1. itemRenderer は与えられたデータをどのように表示するか判断するために、list オーナー(itemRendererが属しているlistコンポーネント) のcriteriaの値をチェックします。
2. Flex の list クラスを継承した listオーナーには、itemRenderer が読み込むことができる public なプロパティがあり、そのプロパティに外部のコード(他のコントロールやActionScriptコード)が値をセットします。
3. list のプロパティがセット(値が変更)されると、そのlistの invalidateList() メソッドが呼び出されます。それが itemRenderer をリフレッシュさせるトリガーとなり、結果としてdataがリセットされます。 ( そしてステップ1へとまた戻ります。)


■ Event

以前の記事で、itemRenderer とアプリケーションの他の部分のデータをやりとりのために、どのようにイベントをバブリングさせれば良いか示しました。 それはそれで簡単で良いのですが、「itemRendererの役割はデータを表現(表示)すること、コントロールの役割はデータを操作すること」という考えに基づいた、より良い方法があるのではないと考えています。

MyTileList コントロールの本質は、売り物の本のカタログを表示することです。ユーザーがある本を選んで購入しようとしたときに、それをアプリケーションに伝えるのはlist コントロールの役割であるべきです。これをコードで書くと次のようになります。

<CatalogList bookBuy="addToCart(event)" />


現在の状況では、イベントはバブルアップして TileList を通り抜けてしまいます。
イベントをバブリングさせる方法では bookBuy イベントを リスト (TileList) コントロールに関連付けないので、コントロールをアプリケーションの別の場所に移動させられます。
例えば、bookBuy イベントのハンドラをメインの Application に記述すると、( その bookBuy イベントをディスパッチする ) リストコントロールをアプリケーションの別の場所に移動させたいとした時に、ハンドラも共に移動しなければいけません。
逆に、もしイベントをリストコントロールに関連付けていれば、そのコントロールを移動させるだけで済みます。

Button のクリック・イベントが実はButtonによりディスパッチされるのではなく、Button 内の別の何か他のものによりディスパッチされてバブリングされてくるものだとしたらどうでしょうか。
<mx:Button click="doLogin()" label="Log in" />とは書けなくなります。 doLogin() メソッドをどこか別の箇所に移動しなければならず実装がややこしくなります。

おわかりいただけたでしょうか。では、イベントをバブリングさせる方法から listコントロールにイベントをディスパッチさせる方法へと、サンプルを変更する手順を説明します。

第1に、CatalogList にメタデータを追加して、コンパイラにそのコントロールがイベントをディスパッチすることを知らせます。

import events.BuyBookEvent;
import mx.controls.TileList;

[Event(name="buyBook",type="events.BuyBookEvent")]

public class CatalogList extends TileList
{


第2に、CatalogList にイベントをディスパッチさせるメソッドを追加します。 このメソッドは itemRenderer により呼び出されます。

Second, add a function to CatalogList to dispatch the event. This function will be called by the itemRenderer instances:

	public function dispatchBuyEvent( item:Object ) : void
{
var event:BuyBookEvent = new BuyBookEvent();
event.bookData = item;
dispatchEvent( event );
}

}


第3に、itemRenderer 内の Buy ボタンが上記メソッドを呼び出すようにコードを変更します。

Third, change the Buy button code in the itemRenderer to invoke the function:

			<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]">
<mx:click>
<![CDATA[
(listData.owner as CatalogList).dispatchBuyEvent(data);
]]>
</mx:click>
</mx:Button>


これで itemRenderer 内のボタンは、dataを引数としてlistコントロールのdispatchBuyEvent() メソッドを呼び出すことができるようになりました。
そうすることにより、アプリケーションの他の部分とのデータのやりとりを行うという責務をlistコントロールに移しています。

今回のサンプルの list コントロールは、data を保持するイベントをディスパッチします。
ActionScriptとMXML(CatalogList.as ファイルで[Event]メタデータで指定されているので)のどちらでも好きな方を使って、アプリケーションはこのイベントに対するイベントリスナをセットすることができます。
[Event]メタデータを使うと、あなたのコードは他の開発者にとって使いやすくなります。

■ Summary

itemRenderer は、イベントを用いてアクションを伝達するべきです。 カスタム・イベントを用いることによりデータを受け渡すことができるので、イベントを受け取る側はわざわざ itemRenderer にまでデータを取得しにいく必要がありません。

itemRenderer は set data メソッドをオーバーライドして、その data の変更に反応するようにしなければいけません。
そのメソッド内では listData.owner から必要なデータにアクセスできます。 また static なクラスや、メインのアプリケーション( Application.application )に保存されたデータにもアクセスできます。

次回の記事では、itemRenderer の状態について見ていきます。


banana_systems at 12:22コメント(0)トラックバック(0)  この記事をクリップ!

2008年07月16日

   itemRenderer パート2 : external itemRenderer をはてなブックマークに追加 itemRenderer パート2 : external itemRenderer のはてなブックマーク数 この記事をクリップ! Yahoo!ブックマークに登録

itemRenderer パート2 : external itemRenderer

itemRendererシリーズのPart1では、inline itemRenderer について説明しました。
inline itemRendererとは、MXML タグと ActionScript のコードが itemRenderer を適用するコンポーネントと同じファイルに存在するものです。

inline itemRenderer は別ファイルで定義されたクラスと同等だと説明しましたよね。
実際にFlex コンパイラは内部的に、インラインで書かれたコードを抜き出してクラスを作成しています。

今回の記事では、自分でそのクラスを作ってみます。
inline itemRendererの利点は、itemRendererを適用するコンポーネントと同じ個所に記述できるというものですが、itemRendererが複雑になってしまったときに読みづらいという欠点にもなります。

itemRenderer を外部ファイルに切り出すことには、いくつかの利点があります。
・itemRendererを複数のリストで利用できる
・メンテナンス性に優れる
・Flex Builderのデザイン・ビューで大枠をデザインすることができる


■An MXML itemRenderer

前回の記事では、DataGrid用に少々複雑なitemRendererを紹介しました。

<mx:DataGridColumn headerText="Title" dataField="title">
<mx:itemRenderer>
<mx:Component>
<mx:HBox paddingLeft="2">
<mx:Script>
<![CDATA[
override public function set data( value:Object ) : void {
super.data = value;
var today:Number = (new Date()).time;
var pubDate:Number = Date.parse(data.date);
if( pubDate > today ) setStyle("backgroundColor",0xff99ff);
else setStyle("backgroundColor",0xffffff);
}
]]>
</mx:Script>
<mx:Image source="{data.image}" width="50" height="50" scaleContent="true" />
<mx:Text width="100%" text="{data.title}" />
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>


itemRendererの基本はHBoxであり、Image とText を含んでいます。背景色は item レコードの pubDate フィールドの値によって決まります。
これと同じitemRendererを外部ファイルとして書く方法を説明しましょう。

1. もし Flex Builder を使っているのなら、新規 MXML コンポーネントを作成してください。(コンポーネント名は GridColumnSimpleRenderer とします。好きな名前を付けてもらって結構です。) ルートタグは HBox とします。サイズは気にする必要はありません。
2. もし SDK を使っているのなら、新規 MXML ファイルを作成してください。ファイル名は GridColumnSimpleRenderer.mxml としました。ルートタグは HBox とします。
3. 新規作成したファイルに上記コードの <mx:HBox>タグ内を全てコピーしてください。( <mx:HBox>タグ自体は新規作成したファイルに存在するものなのでコピーする必要はありません。 )

結果は以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
<mx:Script>
<![CDATA[
override public function set data( value:Object ) : void {
super.data = value;
var today:Number = (new Date()).time;
var pubDate:Number = Date.parse(data.date);
if( pubDate > today ) setStyle("backgroundColor",0xff99ff);
else setStyle("backgroundColor",0xffffff);
}
]]>
</mx:Script>
<mx:Image source="{data.image}" width="50" height="50" scaleContent="true" />
<mx:Text width="100%" text="{data.title}" />
</mx:HBox>


4. ファイルを保存します。

DataGridColumn の定義からインライン itemRenderer を削除して、下記コードと入れ替えます。

<mx:DataGridColumn headerText="Title" dataField="title" itemRenderer="GridColumnSimpleRenderer">


アプリケーションを実行してみると、おっと..。DataGridの行の縦サイズが大きすぎますね。
これは itemRenderer の height プロパティに 300 が指定されているからです。

■ itemRenderer の width と height について

List コントロールは itemRenderer の width を自動で調節します。従って、今回の例の width="400" という指定は無視されます。
ユーザがカラムやリストの幅を変更するとitemRenderer の幅も変わってしまうのを考慮して、itemRendererを実装してください。

heightに関しては話が別です。もし List の rowHeight プロパティに明示的に値を指定すると、全ての行がその指定した高さになり、itemRenderer で指定した height は無視されます。
しかし、List の variableRowHeight プロパティに true をセットすると、itemRenderer に指定した height が優先されます。
今回の例では height に 300 をセットしているので、各行の高さは 300 ピクセルになります。

itemRendererから明示的に値を指定しているheightを削除することにより、正常に表示されるようになります。

■ Dynamically Changing the itemRenderer

今回の例では set data メソッドをオーバーライドし itemRenderer の背景色の変更する処理を行っています。
これはよく使う方法です。
set data メソッドをオーバーライドすることにより、新しい行のデータが data プロパティにセットされたタイミングで独自の処理を行わせることができます。(今回の例では、styleを変更しています)

よくあるミスとして:

・super.data = value; の記述を忘れてしまう。 これはとても重要です。この記述を忘れてしまうと itemRenderer の挙動がおかしくなってしまいます。

・条件に合致しなかったときに styleを戻す処理を忘れてしまう。 今回の例では、pubDate が未来の日付の時だけ背景色を変更し、過去の日付の時に背景色をデフォルトに戻し忘れてしまう。
itemRendererは再利用されるので、必ずデフォルトに戻すといった処理を含めてください。


■ ActionScript itemRenderer

さて、もう一つ itemRenderer を書いてみましょう。今度は ActionScript のクラスを使います。
前回の記事で、次のようなインライン itemRenderer を持つ TileList を使用しました:

<mx:itemRenderer>
<mx:Component>
<mx:HBox verticalAlign="top">
<mx:Image source="{data.image}" />
<mx:VBox height="115" verticalAlign="top" verticalGap="0">
<mx:Text text="{data.title}" fontWeight="bold" width="100%"/>
<mx:Spacer height="20" />
<mx:Label text="{data.author}" />
<mx:Label text="Available {data.date}" />
<mx:Spacer height="100%" />
<mx:HBox width="100%" horizontalAlign="right">
<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]">
<mx:click>
<![CDATA[
var e:BuyBookEvent = new BuyBookEvent();
e.bookData = data;
dispatchEvent(e);
]]>
</mx:click>
</mx:Button>
</mx:HBox>
</mx:VBox>
</mx:HBox>
</mx:Component>
</mx:itemRenderer>


この itemRenderer を外部ファイルの ActionScript で記述します。 以下のステップに従って進めてください。

1. ActionScript クラスの作成. クラス名は BookTileRenderer にし、インラインitemRendererと同様 HBox を継承します。

package
{
import flash.events.MouseEvent;

import mx.containers.HBox;
import mx.containers.VBox;
import mx.controls.Button;
import mx.controls.Image;
import mx.controls.Label;
import mx.controls.Spacer;
import mx.controls.Text;

public class BookTileRenderer extends HBox
{
public function BookTileRenderer()
{
super();
}

}
}


2. 子コンポーネントへの参照を保持するメンバ変数を定義します。

private var coverImage:Image;
private var titleText:Text;
private var spacer1:Spacer;
private var authorLabel:Label;
private var pubdateLabel:Label;
private var spacer2:Spacer;
private var buyButton:Button;


3. createChildren() メソッドをオーバーライドし、子コンポーネントをインスタンス化しHBoxに追加していきます。

override protected function createChildren():void
{
coverImage = new Image();
addChild(coverImage);

var innerBox:VBox = new VBox();
innerBox.explicitHeight = 115;
innerBox.percentWidth = 100;
innerBox.setStyle("verticalAlign","top");
innerBox.setStyle("verticalGap", 0);
addChild(innerBox);

titleText = new Text();
titleText.setStyle("fontWeight","bold");
titleText.percentWidth = 100;
innerBox.addChild(titleText);

spacer1 = new Spacer();
spacer1.explicitHeight = 20;
innerBox.addChild(spacer1);

authorLabel = new Label();
innerBox.addChild(authorLabel);

pubdateLabel = new Label();
innerBox.addChild(pubdateLabel);

spacer2 = new Spacer();
spacer2.percentHeight = 100;
innerBox.addChild(spacer2);

var buttonBox:HBox = new HBox();
buttonBox.percentWidth = 100;
buttonBox.setStyle("horizontalAlign","right");
innerBox.addChild(buttonBox);

buyButton = new Button();
buyButton.label = "Buy";
buyButton.setStyle("fillColors",[0x99ff99,0x99ff99]);
buyButton.addEventListener(MouseEvent.CLICK, handleBuyClick);
buttonBox.addChild(buyButton);
}


親と子の関係が分かりやすくなるように、インデントを入れてみました。 Buyボタンへのクリック・イベントのリスナーの追加も忘れずに行ってください。

4. commitProperties() メソッドをオーバーライドし、子コンポーネントに data の値をセットします。

override protected function commitProperties():void
{
super.commitProperties();

coverImage.source = data.image;
titleText.text = data.title;
authorLabel.text = data.author;
pubdateLabel.text = data.date;
}


5. Buyボタンのクリック・イベントのハンドラーを追加します。

private function handleBuyClick( event:MouseEvent ) : void
{
var e:BuyBookEvent = new BuyBookEvent();
e.bookData = data;
dispatchEvent(e);
}


6. メインのアプリケーションの TileList の itemRenderer を、 ActionScript で記述したものに変更します。
inline itemRenderer を削除して、TileList タグ内の itemRenderer プロパティに新たな itemRenderer のクラス名を記述します。


<mx:TileList id="mylist" x="29" y="542" width="694" itemRenderer="BookTileRenderer" 
dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" >


もしFlexの既存のコンテナクラス(HBoxなど)を基底クラスとするのであれば、私はわざわざ ActionScript を用いてitemRendererを作ろうとはしません。
見て分かる通りMXMLで作成したものと比べてとても複雑になりますし、実のところパフォーマンスもそれほどよくなりませんから。


■ 再利用可能な itemRenderer

次のコードは、CurrencyFormatterを用いて数字を表示する itemRenderer です。ファイル名を PriceFormatter.mxml とします。

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

<mx:Script>
<![CDATA[
import mx.controls.dataGridClasses.DataGridListData;

[Bindable] private var formattedValue:String;

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

formattedValue = cfmt.format( Number(data[(listData as DataGridListData).dataField]) );
}
]]>
</mx:Script>

<mx:CurrencyFormatter precision="2" id="cfmt" />

<mx:text>{formattedValue}</mx:text>

</mx:Text>


この itemRenderer のポイントは赤字の部分です。ここで、バインダブルな formattedValue 変数をセットしています。
まず、cfmt という id の <mx:CurrentFormatter>タグがあるのが分かると思います(ここも ActionScriptで記述しても構いません)。
上記のコードでは CurrentFormatter の format() メソッドを呼び出した結果を formattedValue にセットしています。

format() メソッドは引数にNumber型のオブジェクトを取るので、引数をNumberにキャストしています。
なぜなら、リストのdataProviderはXMLであり、XMLに含まれるものは全てテキストだからです。
data として Object を使えば、これを数値型の値にすることができますが、Number型にキャストして問題になることはありません。

ご存じの通り、data とは itemRenderer で表示する値を保持した単なるプロパティです。よって、[ ] 表記を用いてデータのフィールドにアクセスすることも可能です。
例えば、data['price'] は金額のカラムです。
しかし、itemRenderer を再利用するには、特定のカラム名を指定するのではなく、もっと汎用的なデータの指定方法が必要となります。

そこで listData の登場です。IDropInListItemRenderer インターフェイスを実装する全ての Flex コンポーネントは、listData プロパティを持っています。

Text, Label, Button, CheckBox などほぼ全てのコントールは IDropInListItemRenderer インターフェイスを実装しています。
一方、HBox, Canvas などほぼ全てのコンテナは、IDropInListItemRenderer インターフェイスを実装していません。
もしコンテナクラスを継承した itemRenderer で listData を使いたければ、自前でIDropInListItemRendererを実装しなければなりません。
これについては次回の記事で説明します。

itemRenderer に与えられた listData には、rowIndex プロパティや、自身(itemRenderer)を保持している DataGrid, List, TileList などのコンポーネントへの参照である owner プロパティが存在します。

DataGrid で itemRenderer を使用すると、listData は実際には DataGridListData 型のオブジェクトになります。
DataGridListData には、columnIndex プロパティや カラム (DataGridColumn) の名前を表す dataFiled プロパティなどもあります。

先ほどの行を、内側から一つ一つ説明していきましょう。

* listData as DataGridListData - listData を DataGridListData にキャストすることにより dataField プロパティにアクセスできるようにします。
* .dataField - そのカラムに表示されるフィールドです。 これを用いることにより itemRenderer を汎用的にできます。 dataField を用いることで、この itemRenderer を複数のカラムに使用できます。 今回の例では、dataField プロパティの値は 'price' です。
* data[ ... ] - 与えられたレコードの特定のフィールドにアクセスします。今回の例では、price カラムです。
* Number( ... ) - format() メソッドはNumber型の引数を取るので、Number型へのキャストを行っています。
* cfmt.format( ... ) - 引数に与えられた値を通貨の表記に変換しています。

■ Summary

itemRenderer を実装する方法(inline, external)を説明しましたが、自分が実装しやすい方法で良いと思います。
ActionScriptだけを使う人もいます。FlexとActionScriptの経験を積めば、この方法がうまく行くでしょう。
MXML はシンプルな itemRenderer を手早く仕上げるのに向いています.

今後は UIComponentを継承した ActionScript クラスとして記述したもっと効率的なitemRendererを作成していきます。
次回の記事では、itemRenderer とアプリケーションの他の部分とのデータのやりとりについて説明します。


banana_systems at 23:03コメント(0)トラックバック(0)  この記事をクリップ!

2008年07月06日

   itemRenderer パート1 : inline itemRenderer をはてなブックマークに追加 itemRenderer パート1 : inline itemRenderer のはてなブックマーク数 この記事をクリップ! Yahoo!ブックマークに登録

itemRenderer パート1 : inline itemRenderer

Flex / AIR のリスト系コンポーネント(List, DataGrid, TileListなど)はデフォルトのままでも各アイテム(行)を十分を奇麗に表示してくれるのですが、プロジェクトによってはデフォルトのままの表現では要求を満たさなかったり、ボタンを追加する必要がでてきたり等問題が発生します。

そこで、今のうちにしっかりとマスターしておこうということで Adobe Customer Care の Peter Ent さんのブログである itemRenderer シリーズを翻訳してみました。
今回は itemRenderer パート1 : inline itemRenderer のみの翻訳となっていますが、これから週に1、2回のペースで順次追加していきます。

○ 原文 itemRenderers: Part 1: inline renderers
http://weblogs.macromedia.com/pent/archives/2008/03/itemrenderers_p.html

■ Renderer の再利用

サーバーから受け取ったデータの値に基づき DataGrid コンポーネントの5行目X4列目のセルを緑色に変更する時などに、外部から itemRenderer にアクセスしようとしているのをよく見かけます。
しかし、外部から itemRenderer を取得し、それらを変更するのは Flex フレームワークとFlex コンポーネント・モデルに大きく違反しています。

itemRenderer を理解するには、まずそれが何なのか、また私たち Adobe Flex エンジニアリング・チームが何を意図してそれをデザインしたのかを理解する必要があります。
1000個レコードを見せたい時に Listコンポーネントが 1000個の itemRenderer を作成すると考えるのは誤りです。

もし Listコンポーネントが10行表示するとしたのなら、約12個の itemRenderer が作成されます。
その表示される10行分のitemRendererが10個と、バッファリング及びパフォーマンスのために2、3個作られます。
Listコンポーネントは初期時に1〜10行目を表示しますが、List をスクロールし3〜12行目が表示されたとしても、元々の同じ12個の itemRenderer が用いられます。
スクロールしたとしても新たな itemRenderer が作成されることはありません。

Listコンポーネントがスクロールされると、3〜10行目を表示しているitemRendererは元のデータを表示し続け、位置だけが上へ移動します。
1行目と2行目を表示していた itemRenderer は、下へ移動し11行目と12行目を表示します。
Listのサイズを変更しない限り、itemRendererは新しいデータを表示するために位置移動するだけでインスタンスは再利用されます。



5行目X4列目のセルの背景色を変更する際、既にユーザが Listをスクロールしてしまっていて、そのセルの itemRenderer が21行目のデータを表示しているかもしれないので注意してください。


では、どのようにすればよいのでしょうか?

itemRenderer は、受け取ったデータの値に基づいて自身を変更しなければいけません。

もしListのitemRendererがデータの値に基づいて色を変えるものであれば、そのitemRendererは自身が受け取ったデータを精査し、自分自身を変更しなければいけません。


■ inline itemRenderer ( インライン アイテムレンダラ )

今回の記事では、inline itemRendererを用いて上記の問題を解決していきます。
inline itemRendererとは、MXMLファイル内のitemRendererを適応するコンポーネントの箇所に直接記述されたものです。
次回の記事では、external itemRendererを用います。

inline itemRendererはシンプルなものなので、一般的に簡単なレンダラーを作成する時、又は大きいアプリケーションのプロトタイピング時に用いられます。

inline itemRendererを用いることに何も問題は無いのですが、アプリケーションのコードが複雑になってきた時には一つのクラスとして独立させるほうが好ましいです。

今回の記事では全ての例にて同じデータを用います。データはbookを表し、author, title, publication date, thumbnail image等の値が存在します。
各レコードはXMLノードであり、下記のようなものとなります。
<book>
<author>Peter F. Hamilton</author>
<title>Pandora's Star</title>
<image>assets/pandoras_star_.jpg</image>
<date>Dec 3, 2004</date>
</book>


それでは <mx:List> コントロールの簡単なitemRendererを作ってみます。
表示されるものはauthor、続いてtitleとなります。
<mx:List x="29" y="67" dataProvider="{testData.book}" width="286" height="190">
<mx:itemRenderer>
<mx:Component>
<mx:Label text="{data.author}: {data.title}" />
</mx:Component>
</mx:itemRenderer>
</mx:List>


この itemRenderer はとてもシンプルなので labelFunctionを用いたほうが便利かもしれませんが、重要な点も示しています。
まず一つ目は、inlineRenderer を定義するために <mx:itemRenderer> タグを用いていること。
そして、そのタグ内に <mx:Component> タグが用いられていること。
この <mx:Component> タグは、Flex コンパイラにコンポーネントをインラインにて定義することを知らせます。
そのタグの意味を少し説明しておきます。

itemRendererは<mx:Component> タグ内に定義します。上の例では、<mx:Label> コントロールを1つ用いて、そのコントロールの
textフィールドにデータ・バインディング {data.author}: {data.title} にて値をセットしています。これはとても重要です。 List コントロールは、itemRenderer の data プロパティに値をセットすることにより、各 itemRenderer のインスタンスに dataProvider のレコード(各行)を渡すのです。
上記コードでは、Listの全ての行にて、inline itemRenderer のインスタンスが data プロパティを持ち、そのプロパティに <book> XMLノードがセットされます。
List コントロールをスクロール度に、新しく表示される行のために itemRenderer が再利用されて data プロパティの値が変化します。

別の言い方をすると、1行目の itemRenderer のインスタンスが data.author に"Peter F, Hamilton"がセットされているとしても、表示外にスクロールされると
そのインスタンスは再利用されて "J.K, Rowling" がセットされるかもしれません。 これらは全て自動で処理されるので心配する必要はありません。

次の例は、<mx:List> コントロールを用いた少々複雑なinline itemRenderer です。

<mx:List x="372" y="67" width="351" height="190" variableRowHeight="true" dataProvider="{testData.book}">
<mx:itemRenderer>
<mx:Component>
<mx:HBox >
<mx:Image source="{data.image}" width="50" height="50" scaleContent="true" />
<mx:Label text="{data.author}" width="125" />
<mx:Text text="{data.title}" width="100%" />
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
</mx:List>


先のコードの <mx:Label> が <mx:HBox>と<mx:Image>、<mx:Label>、<mx:Text>に変わった以外あまり違いはありません。
レコードとのデータ・バインディングも有効です。

■DataGrid

DataGrid にも inline itemRenderer が使用可能です。下記コードは DataGridColumn に用いた例です。

<mx:DataGrid x="29" y="303" width="694" height="190" dataProvider="{testData.book}" variableRowHeight="true">
<mx:columns>
<mx:DataGridColumn headerText="Pub Date" dataField="date" width="85" />
<mx:DataGridColumn headerText="Author" dataField="author" width="125"/>
<mx:DataGridColumn headerText="Title" dataField="title">
<mx:itemRenderer>
<mx:Component>
<mx:HBox paddingLeft="2">
<mx:Script>
<![CDATA[
override public function set data( value:Object ) : void {
super.data = value;
var today:Number = (new Date()).time;
var pubDate:Number = Date.parse(data.date);
if( pubDate > today ) setStyle("backgroundColor",0xff99ff);
else setStyle("backgroundColor",0xffffff);
}
]]>
</mx:Script>
<mx:Image source="{data.image}" width="50" height="50" scaleContent="true" />
<mx:Text width="100%" text="{data.title}" />
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>


先の2つの例よりもかなり複雑ですが、itemRenderer を適応するタグの中で <mx:itemRenderer>と<mx:Component> が 定義されているという点では同じ構成になっているのが分かると思います。

<mx:Component>の目的は、MXML内で ActionScript のクラスを作成するためです。
<mx:Component> タグ内のものを別のファイルにコピーしてクラス名を付けるのをイメージしてみてください。
inline itemRenderer は完全な MXML ファイルのようですよね。 ルートタグ ( 上記コードでは<mx:HBox> ) があり、<mx:Script> タグもあります。

上記コードの <mx:Script> タグの目的は、data プロパティの set 関数をオーバーライドすることにより、itemRenderer の背景色を変えるためです。
今回の例では、発行日(pubDate)が未来の場合に背景色を白(0xffffff)から紫(0xff99ff)にします。
itemRenderer は再利用されるので、発行日が過去の場合には白に戻す必要があります。
この色を戻すという処理を行わなければ、ユーザがListを何度もスクロールしていると最終的に全ての itemRenderer の背景色が紫になってしまいます。


■ outerDocument

スコープも変わります。 何を意味しているかというと、<mx:Component> タグ内で定義した変数のスコープは、そのコンポーネント/inline itemRenderer 内のみとなります。
同じように <mx:Component> タグの外のものは別ファイルにて定義されたコンポーネントのように別スコープとなります。
例えば、クリックすると本を購入できる Button を itemRenderer に追加するとします。
クリックイベントに反応し購入処理を行うメソッドをコールする Button は、下記のように定義できます。

<mx:Button label="Buy" click="buyBook(data)" />


もし buyButton() メソッドがファイルの <mx:Script> タグ内に定義されていると「buyBook() は定義されていないメソッドです」という旨のエラーが発生します。
これは butyBook() が <mx:Component> 内のスコープではなく、ファイルのスコープにて定義されているからです。これはよくあるケースなので outerDocument 識別子
を用いて回避してみます。

<mx:Button label="Buy" click="outerDocument.buyBook(data)" />


outerDocument 識別子を用いてファイルもしくは外側のドキュメントを参照できます。
注意事項として、outerDocumentを用いてアクセスするメソッドは protected や private ではなく、public でなければいけません。
<mx:Component> は外部で定義されたクラスのように扱われるのを思い出してください。


■ Bubbling Events

更に複雑な例を見ていきましょう。下記コードは、同じデータを TileList を用いて表示します。

<mx:TileList x="29" y="542" width="694" dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" >
<mx:itemRenderer>
<mx:Component>
<mx:HBox verticalAlign="top">
<mx:Image source="{data.image}" />
<mx:VBox height="115" verticalAlign="top" verticalGap="0">
<mx:Text text="{data.title}" fontWeight="bold" width="100%"/>
<mx:Spacer height="20" />
<mx:Label text="{data.author}" />
<mx:Label text="Available {data.date}" />
<mx:Spacer height="100%" />
<mx:HBox width="100%" horizontalAlign="right">
<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]">
<mx:click>
<![CDATA[
var e:BuyBookEvent = new BuyBookEvent();
e.bookData = data;
dispatchEvent(e);
]]>
</mx:click>
</mx:Button>
</mx:HBox>
</mx:VBox>
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
</mx:TileList>


実行すると itemRenderer は下のように表示されます。

TileListItemRenderer







DataGrid で用いた itemRenderer と似ていますが、Button のクリック・イベントの発生時に outerDocument を通じて buyBook() メソッドを呼び出していません。
今回の例では、クリック・イベントの発生時にカスタムのイベントをディスパッチしています。
そのイベントはバブルアップし、itemRenderer を抜け TileList を通り上位のヴィジュアル・コンポーネントによって受け取られます。


itemRenderer にButton, LinkButton 等のインタラクティブなコントロールを含ませることがよくあります。行を削除したり、今回のケースでは本の購入などクリックした時に何か処理を行わせるといった具合にです。


しかし itemRenderer に何か処理を行わせるのは賢明ではありません。itemRenderer の本来の役割は、見た目を良くすることだからです。
イベントをバブリングさせることで itemRenderer は別のものに処理を渡すことが可能になります。
ここでカスタム・イベントが有用となります。何故ならイベントは各行の data に紐付いているので、そのイベントに data を含めてディスパッチすればいいからです。
そうすることにより、イベントの受け取る側は data を取得するために、わざわざ data を探しにいく必要がなくなるからです。

■Summary

inline itemRenderer を用いることにより List の見た目を簡単に変更できます。

inline itemRenderer は別ファイルで定義された ActionScript のクラスのようなものとして扱ってください。 スコープも別ファイルで定義された ActionScript のクラスと同等となります。
itemRenderer のインタラクションの結果としてデータのやりとりが必要であれば、カスタムイベントを使ってください。

重要 : itemRenderer は再利用されるものなので保持しようせずに、各 itemRenderer に与えられたデータのみを扱うようにしてください。

次回は external itemRenderer を見ていきます。


banana_systems at 17:17コメント(0)トラックバック(0)  この記事をクリップ!
ブログに関しまして
本ブログの記事は
Banana Systems 株式会社
がお届けしております
最新記事
Archives
Categories
TagCloud
QRコード
QRコード
  • RSS